一、前言
这个问题涉及了垃圾回收的内部机制,在通常情况下程序员并不需要去关心和干涉GC的内部执行,但是理解其算法,可以帮助程序员理解哪些代码是高效的,而哪些代码是需要避免的。
二、什么是代
GC在执行垃圾回收时,并不是每次都扫描托管堆内的所有对象实例,这样做太耗费时间而且也没有必要。简单来说,GC会把所有托管堆内的对象按照其已经不再被使用的可能性分成三类,并且从最有可能不被使用的类别开始扫描,.NET对这样的分类类别有一个称呼:代。GC会把所有的托管堆内对象分为3代:0代、1代和2代,其基本机制如下:
- 并不是每次垃圾回收都会同时回收3个代的所有对象,越小的代拥有着越多被释放的机会。CLR的基本算法是:每执行N次0代的回收,才会执行一次1代的回收,而每执行N次1代的回收,才会执行一次2代的回收。
- 当某个对象实例在GC执行时被发现仍然在使用,它将被移动到下一个代上。新分配的对象实例属于0代。
- 根据.NET的垃圾回收机制,0代、1代和2代的初始分配空间分别为256KB,2MB和10MB。
垃圾回收中代的设计,是参考了这样一个事实:一个对象实例存活的时间越长,那它就具有更大的几率去存活更长的时间。而反过来理解,最有可能马上就不被使用的对象实例,往往是那些刚刚才被分配的对象实例,而且新分配的对象实例通常都会被马上大量地使用。这就是为什么0代对象拥有最多被释放的机会,并且.NET只为0代分配了一块相当小的逻辑内存,以使得0代对象的处理有机会被全部放入处理器的缓存中取,这样做的结果是使用频率最高并且最有可能马上可以释放的对象实例拥有了最高的使用频率和最快的释放速度。
相对于0代的快速释放,1代和2代的对象将具有较少被释放的机会。需要注意的是,当一次GC回收发现扔在被使用的对象实例时,会把它移到更高的代上。这就需要程序员避免保留已经不再被使用的对象引用,把对象的引用设置为null是告诉.NET该对象不需要再使用的最直接的方法。
我们现在再回头分析一个Finalize方法,就可以知道它为何会大幅度地影响性能了。在前面已经讲述了需要执行Finalize方法的对象被回收时的具体步骤:它会被暂时视为正在被使用而驻留在堆中,且至少要等一个GC循环才能被释放,这取决于执行Finalize方法的线程的执行速度。很显然,需要执行Finalize方法的那些对象实例,被真正释放时最乐观的情况下也已经处在1代的位置上了,而如果它们是在1代上才开始释放或者执行Finalize方法的线程运行得慢了一点,那该对象就将在2代上才被释放,相对于0代,这样的对象实例在堆中存留的时间将长得多。
三、总结
垃圾回收机制按照对象不被使用的可能性把托管堆内的对象分为3代:0代、1代和2代。越小的代有越多被释放的机会,而每一次GC中扔存活的对象实例将被移到下一代上。