本篇目录
1,垃圾回收的基本概念
1.1 小对象堆和大对象堆
我们都知道,CLR将我们的引用类型分配到托管堆上。这里指的托管堆实际是一个笼统的称呼。它实际是由一个小对象堆(small object heap,SOH)和一个大对象堆(large object heap,LOH)组成的。为对象分配空间时,将对象分为小对象(small object)和大对象(large object)。在NET Framework 1.1 和2.0中,超过85,000 bytes的对象就被称为大对象。很显然,LOH用于存储我们的大对象。对于85,000bytes这个数值,并不那么绝对。CLR内部可能会调节这个值,小于这个值的大对象也会分配到LOH中。堆中包含一个NextObjPtr指针,它总是指向堆末端最后一个有效对象之后的位置。
1.2 垃圾回收中的“代”
.Net垃圾回收器采用的是基于代(generagion)的垃圾回收机制。它做出了如下的假设:对象越新,生存期越短;对象越老,生存期越长;回收堆的一部分,速度快于整个堆。共分为3代:0代、1代、2代。最新分配的小对象,都属于0代。垃圾回收执行一次后,仍然存活的对象被划为1代;执行第二次后还能存活的,被划入2代;执行三次及以上还能存活的,还是属于2代(这是最顶级了,不能再高了)。
CLR初始化时,会为每一代初始化一个阀值,例如:0代(256kb),1代(2M),2代(16M)。当每一代的对象超过这个阀值时,就会执行一次垃圾回收。另外要注意,执行1代的垃圾回收会包括0代,执行2代的垃圾回收会包括1代和0代。执行2代的垃圾回收相当于是对整个托管堆的垃圾回收。
代数越小,它的阀值也越小。由于每次超过阀值就会自动执行垃圾回收,所以它的垃圾回收频率自然也最高。
1.3 堆和“代”的关系
小对象堆上创建的对象都属于0代。大对象堆上创建的对象都属于2代。我们不能创建一个1代的对象。只有垃圾回收器回收一次后仍然存活的对象才能成为1代。所以,小对象堆上可能同时存在0,1,2代的对象。
2,啥时执行垃圾回收?
- 第0代满时 ,这是最常见的一种方式。因为随着应用程序代码运行并分配新对象,这个事件会自然而然的发生。
- 代码显示调用System.GC的静态方法。代码显示的请求CLR执行垃圾回收。
- Window报告内存不足。CLR强制执行垃圾回收,尝试释放已经死亡的对象,从而减少应用程序的占用的内存。
- CLR卸载AppDomain。一个AppDomain被卸载,CLR认为该AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收。
- CLR关闭。一个进程正常终止时(不是从任务管理器强制关掉),CLR认为进程中不存在任何根,因此会调用所有对象的Finalize方法。但不会执行压缩和释放内存,进程终止后,由Windows来回收内存。
3,垃圾回收器是如何工作的?
垃圾回收主要由标记(marking)无效对象,压缩(compact),执行终结器(Finalize)队列,回收内存等几个主要步骤。
首先,垃圾回收器必须要知道一个应用程序是否在使用一个对象,那它是怎么知道的呢?这里牵涉到一个概念:根(root)。应用程序都包含一组根,每个根是一个存储位置,其中包含指向引用类型对象的一个指针。这个指针,要么指向托管堆的一个对象,要么为null。包含根的位置只有:静态字段、方法参数、局部变量、CPU寄存器。
3.1标记无效对象。
垃圾回收开始时,会假设堆中所有的对象都是垃圾。然后逐一检查所有根。如果一个根引用了一个对象,就会在该对象的“同步块索引字段”上开启一位标志——对象就是这样被标记的。如果根引用的对象又引用了其他的对象,垃圾回收器会沿着这条路进行递归的标记。没有被标记的对象就成了无效对象。为了避免出现无限循环,对已经标记的对象,就不会沿着这条路走下去。
3.2 压缩阶段。
垃圾回收器线性遍历堆,寻找未标记对象的连续内存块。如果该内存块较小,垃圾回收器会忽略它;如果内存块较大,垃圾回收器就会把非垃圾对象移动到这里以压缩堆。这样就可以使得堆的末端始终腾出足够的空间以分配新的对象。这里有一个例外,大对象堆中进行垃圾回收时不会进行压缩处理,这是由于大对象的移动代价过于昂贵。压缩阶段还会更新一些对象的引用,因为新对象被移动到了新的位置,参照这些对象的引用都需要更新为新的位置。
3.3 终结揭秘
终结(finalization)是CLR提供的一种机制,允许对象在被垃圾回收器回收其内存之前执行一些必要的清理操作。通常是清理一些非托管的资源(如文件,网络连接,互斥体等)。一个类型只要实现了一个叫Finalize的方法,垃圾回收器在回收对象时就会调用这个方法,以保证这个对象在“临死之前还能吃上最后一顿”。对于C#来说,这个方法的定义是在类名前加一个~符号。
public class SomeType { ~SomeType(){ //这里的代码会进入Finalize方法 } }
从表面上看,终结操作似乎很简单:创建一个对象,当它被回收时,调用它的Finalize方法。但一旦深入研究,就会发现它远非那么简单。应用程序在创建对象时,如果发现它的类型定义了Finalize方法,就会在它的构造器调用之前,将该对象的一个指针放到一个终结列表(finalizaton list)中。终结列表是垃圾回收器控制的一个数据结构。如果一个对象被判定为垃圾,垃圾回收器会扫描终结列表,查找指向垃圾对象的指针。如果找到该指针,就会把它移除,并追加到一个freachable队列中。freachable队列也是垃圾回收器的一个内部数据结构,CLR启动一个特殊的高优先级的线程来专门执行freachable队列中对象的Finalize方法。
当一个对象不可达时,垃圾回收器将其视为垃圾。但是,当垃圾回收器将对象的引用从终结列表移至freachable队列时,对象不再被视为垃圾,其内存不能被回收。标记freachable对象时,这些对象的引用类型字段引用的对象也会被递归的标记,所有这些对象又从垃圾回收的过程中“复活”了。然后,垃圾回收器开始压缩内存,特殊的线程清空freachable队列,并执行每个对象的Finalize方法。垃圾回收器下一次调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以,这些对象的内存会被直接回收。在整个过程中,注意可终结的对象执行两次垃圾回收才能释放它们的内存,由于对象可能被提升至另一代,所以可能还不止两次垃圾回收。
4 ,Dispose模式:强制对象清理资源
Finalize方法非常有用,它保证了当托管对象被释放时,本地资源不会泄漏。但Finalize的问题在于,它的调用时间是没有保证的,也就是不确定的。并且它不是一个公共的方法,不能显示的调用它。
类型如果需要显示的、确定的清理资源,需要实现Dispose模式。Dispose模式较为复杂,大致的实现方式如下:
public class SomeType : IDisposable { //这是一个标记,表明对象已经释放 private bool _diposed = false; //这个方法是可选的,它的功能和Dispose方法一样 //主要是为了那些不熟悉Dispose模式的用户调用 public void Close() { Dispose(true); } //该方法实现IDispose的Dispose //调用这个公共方法来确定性地关闭资源 public void Dispose() { Dispose(true); } ~SomeType() { //调用实际执行清理的方法 Dispose(false); } //这个一个执行清理的常用方法 //Finalize,Dispose,Close都要调用这个方法 //如果这个类是密封的(sealed),protected virtual 应改为private protected virtual void Dispose(Boolean disposing) { //如果已经释放,直接返回 if (!_diposed) { if (disposing) { //在这个if语句中,对象正在被显示的dispose,可以访问对象的字段 //因为Finalize方法还没有执行,所以在这里可以释放托管资源 } //在这里释放本地资源(如文件,套接字等) _diposed = true; //阻止调用Finalize方法 GC.SuppressFinalize(this); } } }
有几点需要说明,任何实现了IDispose接口的类型,就相当于声称自己遵循Dispose模式。无参Dispose方法和Close方法应该可以被多次调用而不抛出异常,第二次调用只是简单的返回。对于GC.SuppressFinalize(this);这句代码是为了阻止Finalize方法被再次调用。如果是从Dispose方法和Close方法显示调用来释放对象,确实不应该再调用Finalize方法。但如果没有从这两个方法显示释放对象,对象就会从Finalize方法来释放对象,可能有人会觉得在Finalize方法中再次调用GC.SuppressFinalize(this);会出问题。但这样不会有任何问题,因为对象已经处于被终结过程中。调用GC.SuppressFinalize(this)方法,会设定与this关联的对象的一个标志位,有了这个标志位,CLR就知道从终结列表移除该对象的指针时,不应该将该对象的指针添加到freachable队列,从而阻止调用对象的Finalize方法。