.NET了解GC中的分代机制么?

(1)背景知识

GC的基本算法,大体上都逃不出 标记清除、复制收集 及 引用计数 三种方式以及它们的衍生品。而.NET CLR中的GC机制所采用的的分代机制 也正是 标记清除 的升级衍生品。有关GC的基本算法的介绍,可以参考我的这一篇文章:《内管管理与GC那点事儿》。.

(2).NET GC将垃圾分为三代

在.NET的GC执行垃圾回收时,并不是每次都扫描托管堆内的所有对象实例,这样做太耗费时间而且没有必要。相反,GC会把所有托管堆内的对象按照其已经不再被使用的可能性分为三类,并且从最有可能不被使用的类别开始扫描,.NET对这样的分类类别有一个称呼:代(Generation)

GC会把所有的托管堆内的对象分为0代、1代和2代

第0代,新近分配在堆上的对象,从来没有被垃圾收集过。任何一个新对象,当它第一次被分配在托管堆上时,就是第0代。 

第1代,经历过一次垃圾回收后,依然保留在堆上的对象。 

第2代,经历过两次或以上垃圾回收后,依然保留在堆上的对象。如果第2代对象在进行完垃圾回收后空间仍然不够用,则会抛出OutOfMemoryException异常

对于这三代,我们需要知道的是并不是每次垃圾回收都会同时回收3个代的所有对象,越小的代拥有着越多被释放的机会

(3).NET GC分代的基本算法

CLR对于代的基本算法是:

每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收

当某个对象实例在GC执行时被发现仍然在被使用,它将被移动到下一个代中上,下图简单展示了GC对三个代的回收操作。

.NET了解GC中的分代机制么?

根据.NET的垃圾回收机制,0代、1代和2代的初始分配空间分别为256KB、2M和10M

说完分代的垃圾回收设计,也许我们会有疑问,为什么要这样弄?

其实分代并不是空穴来风的设计,而是参考了这样一个事实:

一个对象实例存活的时间越长,那么它就具有更大的机率去存活更长的时间。

换句话说,最有可能马上就不被使用的对象实例,往往是那些刚刚被分配的对象实例,而且新分配的对象实例通常都会被马上大量地使用。

这也解释了为什么0代对象拥有最多被释放的机会,并且.NET也只为0代分配了一块只有256KB的小块逻辑内存,以使得0代对象有机会被全部放入处理器的缓存中去,这样做的结果就是使用频率最高并且最有可能马上可以被释放的对象实例拥有了最高的使用效率和最快的释放速度。

因为一次GC回收之后仍然被使用的对象会被移动到更高的代上,因此我们需要避免保留已经不再被使用的对象引用,将对象的引用置为null是告诉.NET该对象不需要再使用的最直接的方法

在前面我们提到Finalize方法会大幅影响性能,通过结合对代的理解,我们可以知道:

在带有Finalize方法的对象被回收时,该对象会被视为正在被使用从而被留在托管堆中,且至少要等一个GC循环才能被释放。

画外音:为什么是至少一个?因为这取决于执行Finalize方法的线程的执行速度。

很明显,需要执行Finalize方法的那些对象实例,被真正释放时最乐观的情况下也已经位于1代的位置上了,而如果它们是在1代上才开始释放或者执行Finalize方法的线程运行得慢了一点,那该对象就在第2代上才被释放,相对于0代,这样的对象实例在堆中存留的时间将长很多。