一、写在前面
GC通过从程序的根对象开始遍历来检测一个对象是否可被其他对象访问,而不是用类似于COM中的引用计数方法
GC并不是能释放所有的资源。它不能自动释放非托管资源;GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性
当在程序中通过new关键字在托管堆中分配空间时,gc会对其进行分析,如果该对象含有Finalize方法(定义了~析构函数)则在析构对象表FinalizationQueue中添加一个指向该对象的指针
二、gc流程
gc启动后,通过Mark-Compact算法进行操作:
1、挂起所有线程
2、假定此时堆中所有对象都是垃圾,然后通过CLR在堆之外可以找到的所有入口点为起点roots,roots包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(包括上面的FinalizationQueue)等,主要可以归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register)
3、Mark阶段:根据对象引用关系,从roots出发遍历所以可以到达的对象,创建出reachable_objects_graph,剩余对象即为unreachable,可以被回收
4、对象回收阶段(下一章节)
5、压缩阶段:对象回收之后堆内存空间变得不连续,在堆中移动这些对象,使他们重新从堆基地址开始连续排列,类似于磁盘空间的碎片整理
6、重新开始被挂起的线程
三、回收阶段
在GC被启动以后,经过Mark阶段分辨出哪些是垃圾,这时会在垃圾中搜索,检查垃圾中是否有被FinalizationQueue中的指针所指向的对象,如果没有,则直接回收,如果有,则将这个对象从垃圾中分离出来,并将指向它的指针移动到待析构表FreachableQueue中。
FreachableQueue的作用很简单,它是在一个优先级较高的独立线程中运行的,平时会处于休眠状态,一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,在下个gc周期才会真正被回收。
四、特殊函数ReRegisterForFinalize和SuppressFinalize
.net的System.GC类提供了控制Finalize的两个方法:
ReRegisterForFinalize:请求系统完成对象的Finalize方法,该方法其实就是将指向对象的指针重新添加到FinalizationQueue中
SuppressFinalize:请求系统不要完成对象的Finalize方法。在使用~析构函数的同时又使用了IDisposable接口的时候,Dispose函数在执行完后应该调用 GC.SuppressFinalize以阻止 GC调用Finalize方法,因为Finalize方法的调用会牺牲部分性能
Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer.而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收
五、Generational 分代算法
将对象按照生命周期分成新的、老的,根据统计分布规律所反映的结果,可以对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉
Heap分为3个代龄区域,相应的GC有3种方式: # Gen 0 collections, # Gen 1 collections, # Gen 2 collections。如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收
Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为fullGC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时fullGC可能需要花费几秒时间。大致上来讲.NET应用运行期间2代、1代和0代GC的频率应当大致为1:10:100
参考博文:CSHARP的GC