第六节:终结操作解密

终结操作表面上视乎很简单:创建一个对象,当它被回收时,它的Finalize方法会得到调用。但是一旦深研究下去,就会发现终结操作原非这么简单。

应用程序创建一个新对象时,new操作符会从堆中分配内存。如果对象的类型定义了Finalize方法,那么在该类型的实例构造函数调用之前,会将指向该对象的一个指针放到终结列表(finalization list)中。终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象- 在回收该对象的内存之前,应调用他的Finalize方法。

下图包含了几个对象的堆。有的对象从应用程序的根可达,有的不可达。对象C、E、F、I、j被创建时,系统检测到这些对象的类型定义了Finalize方法,所以指向这些对象的指针添加到终结者列表中

虽然System.Object定义了一个Finalize方法,但是CLR知道忽略它。也就是说,构造一个类的实例时,如果该类型的Finalize方法时从System.Object继续的,就不认为这个对象时可终结的。类型必须重写Object的Finalize方法,这个类以及派生类型的对象才被认为是可终结的。

垃圾回收开始时,对象B,E,G,H,I和J被判定为垃圾。垃圾回收器扫描终结列表以查找指向这些对象的指针。找到指针后,该指针回从终结列别中移除,并追加到freachable队列中,freachable是垃圾回收期的另一个内部数据结构。Freachable队列中的每个指针都代表其Finalize方法已准备好调用一个对象,下图是展示回收完毕之后的托管堆。

B,G,H占用的内存已经被回收,因为他们没有Finalize方法。但是,对象E,I和J占用的内存暂时不能回收,因为他们的Finalize方法还没有调用。

一个特殊的高优先级CLR线程专门负责调用Finalize方法。使用专用的线程可以避免潜在的线程同步问题。Freachable为空时(这是常见的情况),该线程将睡眠。但一旦队列中有对象出现,该线程就会被唤醒,将每一项从freachable队列中移除,并调用每个对象的Finalize方法。由于该线程的特殊工作方式,Finalize中的代码不应该对执行代码的线程做出任何假设。例如,不应该在Finalize方法中访问线程的本地存储。

CLR未来可能使用多个终结者线程。所以,你写的代码不应假设Finalize方法被连续调用。换言之,如果Finalize方法中的代码要接触到共享的状态,就应该使用线程同步锁。在只有一个终结器线程的情况下,可能有多个CPU分配可终结的对象,但是只有一个线程执行Finalize方法,这回造成线程可能跟不上分配的速度,造成性能和伸缩性方面的问题。

终结列表和Finalize之间的交互非常有意思。首先,让我告诉你freachable队列的这个名称的由来。F明显代表终结(Finalization);freachable队列中的每一个记录项都是对托管堆中的一个对象的引用,该对象的Finalize方法被调用。Reachable意味着可达的。换言之,可将freachable队列看成像静态字段那样的一个根。因此,如果一个对象在freachable中,它就是可达的,不是垃圾。

简单的说,当一个对象不可达时,垃圾回收器就把它视为垃圾。但是,当垃圾回收器将对象的引用从终结者列表移到freachable列表中时,对象不再被认为是垃圾,其内存不能被回收。标记freachable对象时,这些对象的引用类型的字段所引用的对象也会被递归标记,所有这些对象都会在垃圾回收过程中存活下来,到这个时候垃圾回收器才结束对垃圾的标记。由于一些原本被认为是垃圾的对象被重认为不是垃圾,所以从某种意义上将,这些对象复活了,然后,垃圾器开始压缩可回收的内存,特殊的CLR线程清空freachable队列,并执行每个对象的Finalize方法。

垃圾回收期下一次调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以这些对象的内存会直接被回收。整个过程中可终结的对象需要执行两次垃圾回收才能释放他们的内存,在实际应用程序中,对象可能被提升到另一代,所以可能要求不止进行两次垃圾回收。

原文地址:https://www.cnblogs.com/bingbinggui/p/4393693.html