第5章 托管和非托管的资源

  • 1.什么是托管与非托管?

托管资源:一般是指被CLR(公共语言运行库)控制的内存资源,这些资源由CLR来管理。可以认为是.net 类库中的资源。

非托管资源:不受CLR控制和管理的资源。

对于托管资源,GC负责垃圾回收。对于非托管资源,GC可以跟踪非托管资源的生存期,但是不知道如何释放它,这时候就要人工进行释放。

2. 后台内存管理

介绍给变量分配内存时在计算机的内存中发生的情况

(1) 值数据类型:

  • 不是对象成员的值数据类型采用栈存储方式。
  • 栈指针表示栈中下一个空闲存储单元的地址。
  • 栈存储是从高内存地址向低内存地址填充。
  • 值数据变量超出作用域时,CLR就知道不再需要这个变量。释放变量时,其顺序总是与给它们分配内存的顺序相反。

(2) 引用数据类型:

  • 用new运算符请求的内存空间(即托管堆)。用于存储一些数据,在方法退出后很长一段时间内数据仍可用。
  • 托管堆是在垃圾回收器GC的控制下工作。
  • 堆工作原理及内存分配:
void DoWork()
{
     Customer arabel;           // 在栈上给这个应用分配存储空间,但仅是一个引用,占用4个字节的空间,而不是实际的Customer对象。
     arabel = new Customer();   // 首先分配堆上的内存,以存储Customer对象;再把变量arabel的值设置为分配给新Customer对象的内存地址。
     // Customer实例没有放在栈中,而是放在堆中
     // 与栈不同,堆上的内存是向上分配的
//------------------------------------------------------------------ Customer otherCustomer2 = new EnhancedCustomer(); //用一行代码在栈上为otherCustomer2引用分配空间,同时在堆上为EnhancedCustomer对象分配空间。 }
  • 引用变量赋值:把一个引用变量的值赋予另一个相同类型的变量,则有两个变量引用内存中的同一个对象。
  • 引用变量回收:当一个引用变量超出作用域是,它会从栈中删除引用,但引用对象的数据仍保留在堆中;一直到程序终止,或GC删除它为止;只有在该数据不再被任何变量引用时,它才会被删除。

(3) 垃圾回收

  • GC运行时,会从堆中删除不再引用的所有对象。
  • GC压缩操作:对于托管堆,在删除了能释放的所有对象后,就会把其他对象移动回堆的顶端,再次形成一个连续的内存块。
  • 堆系列:
    • 第0代:创建新对象时,会把它们移动到堆的这个部分中;驻留最新的对象;对象会继续放在这个部分,知道垃圾回收过程第一次进行回收。
    • 第1代:第一次回收清理过程之后,仍保留的对象会被压缩,并移动到该部分。此时第0代对应的部分为空。
    • 第2代:对于第1代中的老对象,这样的移动会再次发生,则其遗留下来的对象会移动到堆的第2代。位于第0代的对象会移动到第1代,第0代仍用于放置新对象。
    • 在给对象分配内存空间时,如果超出了第0代对应的部分的容量,或调用GC.Collect()方法,就会进行垃圾回收。
  • 有助于提高性能的领域:
    • 大对象堆(大于85 000个字节的对象):架构处理堆上较大对象的方式,其对象不执行压缩过程。第2代和大对象堆的回收放在后台线程上进行,即应用程序线程仅会为第0代和第1代的回收而阻塞,从而减少了总暂停时间。
    • 垃圾回收的平衡:专用于服务器的垃圾回收。平衡小对象堆和大对象堆,可以减少不必要的回收。

3. 强引用和弱引用

  • 强引用:GC不能回收仍在引用的对象的内存。
    • 缺点是强引用实例超出作用域,或指定为null。如果GC在运行,很容易错过引用的清理,不能释放引用的内存。可使用WeakReference避免这种情况。
  • 弱引用:使用WeakReference类创建。使用构造函数,可以传递强引用(Target属性)。
    • 弱引用对象可能在任意时刻被回收,因此引用该对象前必须确认存在(IsAlive属性为true)。成功检索强引用后,可以通过正常方式使用它。
//创建一个DataObject,并传递构造函数返回的弱引用
var myWeakReference = new WeakReference(new DataObject());

if (myWeakReference.IsAlive)
{
    DataObject strongReference = myWeakReference.Target as DataObject;
    if (strongReference != null)
    {
         //使用强引用对象 strongReference
    }    
}
else
{
     // 引用不可用
}

4. 处理非托管的资源

  • GC不知道如何释放非托管资源(如文件句柄、网络连接和数据库连接)
  • 托管类在封装对非托管资源的直接或间接引用时,需制定专门的规则,确保非托管的资源在回收类的一个实例时释放。
  • 自动释放非托管资源的机制:
    • 声明一个析构函数(或终结器),作为类的一个成员
    • 在类中实现System.IDisposable接口

(1) 析构函数或终结器

  • 在C#中定义析构函数时,编译器发送给程序集的实际上是Finalize()方法
  • C#析构函数问题
    • 不确定性:由于GC的工作方式,无法确定C#对象的析构函数何时执行。
    • 会延迟对象最终从内存中删除的时间。
      • 没有析构函数的对象:在GC的一次处理中从内存删除
      • 有析构函数的对象:需要两次才能销毁。

(2) IDisposable接口:推荐使用,为释放非托管的资源提供了确定的机制,精确控制何时释放资源。

  • 一般方式
SqlConnection conn = null;
try
{
    conn = new SqlConnection();
    //do something;
}
finally
{
    conn?.Dispose();
}
  • 改进方式,使用using简化输入,编译器自动翻译成 try...finally。在变量超出作用域是,会自动调用其Dispose()方法。
using(SqlConnection conn = new SqlConnection())
{
      //do something;
}

(3) 双重实现:正确调用Dispose(),同时将析构函数作为一种安全机制。

    public class BaseResource : IDisposable
    {
        private IntPtr _handle; // 句柄,属于非托管资源
        private System.ComponentModel.Component _comp; // 组件,托管资源
        private bool _isDisposed = false; // 是否已释放资源的标志

        //实现接口方法
        //由类的使用者,在外部显示调用,释放类资源
        public void Dispose()
        {
            Dispose(true);// 释放托管和非托管资源

            // 将对象从垃圾回收器链表中移除,
            // 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数
            GC.SuppressFinalize(this);
        }

        //由垃圾回收器调用,释放非托管资源
        ~BaseResource()
        {
            Dispose(false);// 释放非托管资源
        }

        //参数为true表示释放所有资源,只能由使用者调用
        //参数为false表示释放非托管资源,只能由垃圾回收器自动调用
        //如果子类有自己的非托管资源,可以重载这个函数,添加自己的非托管资源的释放
        //但是要记住,重载此函数必须保证调用基类的版本,以保证基类的资源正常释放
        protected virtual void Dispose(bool disposing)
        {
            if (!this._isDisposed)// 如果资源未释放 这个判断主要用了防止对象被多次释放
            {
                if (disposing)
                {
                    // 释放托管资源,调用其Dispose方法 
                    _comp.Dispose();
                }    

                // 释放非托管资源
                closeHandle(_handle);
                _handle= IntPtr.Zero;         
            }
            this._isDisposed = true; // 标识此对象已释放
        }
    }

5.不安全的代码:C#直接访问内存

(1) 用指针直接访问内存:

  • 引用就是一个类型安全的指针。
  • 使用指针可以访问实际内存地址,执行新类型的操作。
  • 使用指针的原因:
    • 向后兼容性:用于调用本地的Windows API => 可使用DllImport声明,以避免使用指针。
    • 性能:指针提供最优速度性能 => 可使用代码配置文件,查找代码中的瓶颈。
  • 使用指针,必须授予代码运行库的代码访问安全机制的高级别信任。
  • 强烈建议不要轻易使用指针。

 (2) 用unsafe关键字编写不安全的代码

  • 方法、方法的参数、类或结构、成员,均可使用unsafe
  • 方法中的一块代码可标记为unsafe
void MyMethod()
{
     // code that doesn't use pointers
     unsafe
     {
           // unsafe code that uses pointers here
     }
     // more 'safe' code that doesn't use pointers
}
  •  不能把局部变量本身标记为unsafe

(3) 指针的语法

  • 把代码块标记为unsafe后,可以使用指针语法声明。
  • 指针运算符
    • & 寻址运算符:表示“取地址”,并把一个值数据类型转换为指针。
    • * 间接寻址运算符:表示“获取地址的内容”,把一个指针转换为值数据类型。
  • 指针可以声明为任意一种值类型,包括结构;但不能声明为类或数组。

(4) 将指针强制转换为整数类型

  • 由于指针实际上存储了一个表示地址的整数,因此任何指针中的地址都可以和任何整数类型之间相互转换。
  • 转换必须是显示指定的。
  • 转换的主要目的是显示指针地址
  • 32位系统,可转换为uint、long、ulong
  • 64位系统,可转换为ulong

(5) 指针类型之间的强制转换

  • 可以在指向不同类型的指针之间进行显式转换
  • 指针类型之间转换,实现C union类型的等价形式

(6) void指针

  • 维护一个指针,但不希望指定它指向的数据类型,可声明为void指针
  • 主要用途:调用需要void*参数的API函数

(7) 指针算术运算

  • 指针加减整数,是指指针存储地址值的变化。
  • 不同类型的指针字节数不同
    • 给类型为T的指针加上数值X,其中指针的值为P,则得到的结果是P+X*(sizeof(T))
    • 如果类型是byte或char,总字节数不是4的倍数,连续值不是默认地存储在连续的存储单元中
  • 对指针使用+、-、+=、-=、++、--,其右边变量必须是long或ulong
  • 不允许对void指针执行算术运算

(8)sizeof运算符:确定各种数据类型的大小。

  • 优点是不必在代码中硬编码数据类型的大小
sizeof(char) = 2; sizeof(bool) = 1;
  • 结构可以使用,类不能使用。

(9)结构指针:指针成员访问运算符

  • 结构不能包含任何引用类型,因为指针不能指向任何引用类型。
  • 指针成员访问运算符: ->
// 结构
struct MyStruct
{
     public long X;
     public float F;  
}

// 结构指针
MyStruct* pStruct;

// 初始化
var myStruct = new MyStruct();
pStruct = &myStruct;

// 通过指针访问结构的成员值
(*pStruct).X = 4;
(*pStruct).F = 3.4f;
// 使用成员访问运算符
pStruct->X = 4;
pStruct->F = 3.4f;

//指针指向结构中一个字段
long* pL = &(pStruct->X);
float* pF = &(pStruct->F);

(10) 类成员指针

  • 对类中值类型成员创建指针,需要使用fixed关键字,告知GC这些对象不能移动。
    • 在执行fixed块中代码时,不能移动对象位置。
    • 声明多个fixed指针,就可以在同一个代码块钱放置多条fixed对象
    • 可以嵌套fixed块
    • 类型相同,可在同一个fixed块中初始化多个变量

(11)使用指针优化性能:创建基于栈的数组。

  • 在栈中的数组具有高性能、低系统开销的特点,但只对于一位数组比较简单。
  • 关键字stackalloc:指示在栈上分配一定量的内存。
    • 要存储的数据类型
    • 要存储的数据项数
int size; 
size = 20; 
double* pDoubles = stackalloc double[size];
    • 分配的字节数是项数乘以sizeof(数据类型)
    • stackalloc返回的指针指向新分配内存块的顶部
    • 数组元素访问:
      • 使用指针算术,即表达式*(pDouble+X)访问数组中下标为X的元素
      • 表达式pDouble[X]在编译时解释为*(pDouble+X)

6.平台调用

  •  并不是Windows API调用的所有特性都可用于.NET Framework,可采用平台调用方法实现。
  • 采用extern修饰符标记
  • 用属性[DllImport]引用DLL
  • 非托管方法定义的参数类型必须用托管代码映射类型
  • C++有不同Boolen数据类型,可使用特性[MarshalAs]指定.NET类型bool应映射为哪个本机类型
// C++ 调用Windows API(kernel32.dll)中CreateHardLink
BOOL CreateHardLink(
       LPCTSTR lpFileName,
       LPCTSTR lpExistingFileName,
       LPSECURITY_ATTRIBUTES lpSecurityAttributes
);

// C# 调用CreateHardLink
[DllImport("kernel32.dll", SetLastError="true",
                     EntryPoint="CreateHardLink", CharSet=CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CreateHardLink(string newFileName,
            string existingFilename,
            IntPtr securityAttributes);
  • 通常本地方法时,通常必须使用Windows句柄(IntPtr结构);NET2.0引入SafeHandle类,派生的句柄类型是SafeFileHandle\SafeWaitHandle\SafeNCryptHandle\SafePipeHandle。

猜你喜欢

转载自www.cnblogs.com/zhangjbravo/p/9546836.html