【C# .Net GC】GC与终结器 Finalize


终结器方法的JIT编译行为不同

要解释这个类型的作用,我们先来了解一下终结方法是干啥的,为什么要定义一个终结方法?

我们定义一个终结方法一般是这个类型“消费”了一些本地资源,这些资源是非托管的,也就是CLR管不了。那类型的开发者就必须显式的销毁这些资源,那我们就给这个类型定义一个终结方法(至于是否该定义终结方法,终结方法到底该怎么用,不在本文讨论之列)。

1、当GC确定一个类型是垃圾的时候,就会着手清理这个类型,但是又发现这个类型实现了一个终结方法,GC就会把这些对象放到一个可终结对象的队列里面,然后一一调用这些对象的终结方法,

2、在调用这些终结方法之前,JIT肯定先要编译这个终结方法(在构造器之前初始化)。System.Object 定义了Finalize方法,但是CLR知道忽略它。类型必须重写Oject的Finalize方法的类型以及派生类型才被认为是可终结的。

3、垃圾回收时候回收器扫描终结列表以查找对这些对象的引用。找到一个引用后,该引用会从终结队列中移除,并附加到F-reachable队列。F-reachable队列是GC内部一种数据结构。。F-reachable队列中的引用都代表其Finalize方法以及准备耗调用一个对象。

4、一个特殊高优先级的CLR线程专门调用Finalize方法。

5、在递归调用中,标记F-reachable对象时,将递归标记对象中的引用类型的字段所引用的对象,所有的这些对象必须复活以便在回收过程中存活 。之后,GC才结束对垃圾的标识。在这过程中原本被认为是垃圾的对象复活了,然后GC压缩可回收对象的内存。接着特殊的终结线程清空Freach队列,执行每个Finalize方法。 注意F-reachable可终结对象需要执行两次的垃圾回收才能释放他们占用的内存。

CriticalFinalizerObject抽象类解决了以上问题

一切直接或间接的从CriticalFinalizerObject派生的类型,在创建的时候JIT就会将这些个类型的终结方法先给编译了。这下好了,你要回收内存的时候可能没有资源即时编译终结方法,那你创建的时候总有资源吧。为了证明这点我们用Visual Studio + SOS来一探究竟,先上示例代码:

详细请查看:

设计一个类型时,出于以下几个性能方面的原因,最好是避免使用Finalize方法。

  • 可终结的对象要花更长的时间来分配,因为指向它们的指针必须放到终结列表中(21.7节“终结操作揭秘”会更详细地讨论)。
  • 可终结的对象会提升到较老的代,这会增大内存压力,并在垃圾回收器判定对象为垃圾时阻止回收对象的内存。除此之外,该对象直接或间接引用的所有对象也会被提升(21.14节“代”将讨论提升和代的问题)。
  • 可终结的对象导致应用程序的运行速度变慢,这是因为每个对象在回收时,必须对它们进行额外的处理。

Finalize方法在垃圾回收结束时调用,有以下5种事件会导致开始垃圾回收。

第0代满第0代满时,垃圾回收会自动开始。该事件是目前导致 Finalize方法被调用的最常见的一种方式,因为随着应用程序代码运行并分配新对象,这个事件会自然而然地发生
代码显式调用System.GC的静态方法Collect代码可以显式请求CLR执行垃圾回收。虽然 Microsoft强烈建议不要这样做,但某些时候还是有必要的。
Windows报告内存不足 CLR内部使用Win32 CreateMemoryResourceNotification和QueryMemoryResourceNotification函数来监视系统的总体内存。如果Windows报告内存不足,CLR将强制执行垃圾回收,尝试释放已经死亡的对象,从而减小进程工作集的大小。
CLR卸载AppDomain一个AppDomain被卸载时,CLR认为该AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收。第22章“CLR寄宿和AppDomain"将讨论AppDomain。

CLR关闭一个进程正常终止时(相对于从外部关闭,比如通过任务管理器关闭),CLR就会关闭。在关闭过程中,CLR会认为该进程中不存在任何根,因此会调用托管堆中的所有对象的Finalize方法。注意,CLR此时不会尝试压缩或释放内存,因为整个进程都要终止,将由Windows负责回收进程的所有内存 。

C