垃圾回收GC:.Net自己主动内存管理 上(三)终结器

垃圾回收GC:.Net自己主动内存管理 上(三)终结器



前言


.Net下的GC全然攻克了开发人员跟踪内存使用以及控制释放内存的窘态。然而,你或午想要理解GC是怎么工作的。此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包括很具体的内在算法描写叙述。同一时候,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理。


终结器



GC提供了另外一个可以给你带来优点的功能:终结器。在一个资源被回收后,终结器同意一个优雅的清理操作。使用终结器,当GC释放资源所占的内存时,它们可以进行适当的自我清理。
      
简而言之:当GC检測到一个对象是垃圾时,GC会调用它的Finalize()方法(假设存在的话),而且回收对象所占内存。看以下的代码:

public class BaseObj {
    public BaseObj() {
    }

    protected override void Finalize() {
        // 实现资源清理的代码
        // 比方,关闭文件或网络连接
        Console.WriteLine("In Finalize."); 
    }
}


如今你能够创建一个此对象的实例:

BaseObj bo = new BaseObj();

在将来假设GC检測到这个对象变成了垃圾,GC会去查看它是否实现了Finalize方法;假设是,就会调用此方法,最后回收此对象所占内存。
     
很多习惯C++的开发人员会迅速把析构器destructor和Finalize方法联系起来。然而,值得注意的是:它们全然不是一回事,当你试图理解终结器时你最好忘记一切有关destructor的操作。托管对象从来没有destructor周期。
当设计一个类型时,最好避免使用Finalize方法。以下列出几个原因:
  • 可被终结(Finalize)的对象会被提升到GC的更老一代中,这会增大内存压力并阻止对象内存回收即使GC觉得此对象为垃圾对象。另外,全部与此对象有直接或间接关系的对象也会被提升。GC中的代以及代的提升在兴许文章中会介绍。
  • 可被终结(Finalize)的对象须要更长时间去分配。
  • 强制GC运行终结(Finalize)方法会明显减少性能。因此,假设有10000个对象实现了Finalize方法,GC必须运行其10000次终结方法,非常伤性能。
  • 实现终结器的对象可能引用了没有终结器的对象,导致了那些没有终结器的对象的生命周期的延长。实际上,你可能会想把一个类型分成两个不同的类型:没有引用不论什么其他对象的带终结器的轻量级类型和引用了其他对象而不带终结器的类型。
  • 你无法控制终结器方法的运行时间。因此,它可能会占有一定的资源不释放直到GC的下次回收。
  • 当一个程序终止时,一些对象始终可以被訪问到而且不会运行其终结器。比方,后台线程使用的对象或者程序终止(或程序域卸载)过程中创建的对象。另外,默认地,为了程序可以终止迅速,当一个程序终止时不会调用无法被訪问到的对象的终结器。当然,全部操作系统资源都会被回收利用,可是在托管堆中的对象是不可以被恰当清理的。假设你想改变这个默认行为,你可以调用System.GC的RequestFinalizeOnShutdown方法。只是,你一定要小心地使用此方法,由于调用此方法意味着你的这个类型正在控制整个应用程序的策略。
  • 程序执行时无法保证终节器的执行顺序。比方,一个对象包括一个指针指向一个内部对象,GC检到这两个对象都是垃圾。更进一步说,内部对象的终结器先被调用。如今,外部对象的终结器可以訪问到内部对象而且调用其方法,可是内部对象已经被终结了。此时,结果是无法预知的。因为这个原因,强烈推荐终结器不要訪问不论什么内部成员对象
      
假设你决定让你的类型实现终结器,必须确保代码能够尽可能快的运行完成。这样能够避免可能会阻止终结器运行的动作,包含线程同步操作。还有一方面,假设你在终结器内抛出了异常,系统会觉得终结器已返回(运行完成),然后继续运行其他对象的结终器。
      
当然编译器为构造器生成代码时,编译器会自己主动地插入一个对基类构造器的调用。相同地,当一个C++编译器为析构器生成代码时,编译器会自己主动地插入一个对基类析构器的调用。然而,就像之前所说,终结器和析构器是不同的。编译器对于终结器没有特殊处理,因此编译器不会自己主动生成代码去调用基类的终结器。假设你想实现这个过程,你必须明白地在你的终结器里调用基类终结器:
public class BaseObj {
    public BaseObj() {
    }

    protected override void Finalize() {
        Console.WriteLine("In Finalize."); 
        base.Finalize();    // 调用基类终结器
    }
}
      
在衍生类型终结器中你会常常在最后一句代码调用基类的终结器。这样能够保持基类对象尽可能存在的更久。由于这样的调用基类终结器比較常见,C#中有一个简单的语法:

class MyObject {
    ~MyObject() {
        //其他代码
    }
}
causes the compiler to generate this code: 
class MyObject {
    protected override void Finalize() {
        //其他代码
        base.Finalize();
    }
}


这和C++析构器有些像,可是记住C#不支持析构器。


终结器内部



表面上,终结器看起来直接了当:你创建一个带终结器的对象,当它被回收时,终结器被调用。实际上,有很多其它的操作你看不到。
      

当一个应用程序创建一个新的对象,new操作符在堆中给它分配内存。假设有终结器,一个指向此对象的指针会被放入终结器队列。终结器队列是一个由GC控制的内部数据结构。队列中的每一项指向一个对象,而此对象在内存回收前会调用终结器。

      下图中堆中存放着几个对象。一些对象能够被程序根訪问到,一些不能。当对象C,E,F,I和J被创建,系统检測到这些对象实现了终结器,同一时候在终结器队列里加入了指向这些对象的指针。

带有非常多对象的托管堆(对堆与栈疑惑的能够參考:深入浅出图解C#堆与栈)
技术分享
Finalization Queue:终结器队列;


当GC回收内存时,对象B,E,G,H,I和J被觉得是垃圾。GC扫描终结器队列看是否存在指针指向这些对象。假设存在,在终结器队列中移除此指针并把它移动到终结器可达队列。终结器可达队列是另外一个由GC控制的内部数据结构。终结器可达队列中的每个指针代表一个已经运行过终结器的对象。
      
在GC回收内存后,托管堆变成了下图。你能够看到对象B,G和H所占的内存已经被回收利用由于它们没有终结器。然而,对象E,I和J所占的内存没有被回收由于它们的终结器还没有被运行。

GC回收之后的托管堆:
技术分享
Finalization Queue:终结器队列;Freachable Queue: 终结器可达队列

 
有一个特殊执行时线程专门用于调用终结器。通常情况下,当终结器可达队列是空队列时,这个线程进入休眠。可是,一旦终结器可达队列出现新的项,此线程苏醒,移除终结器可达队列中全部项并调用它们的终结器。因为这个机制的存在,你不可在终结器中执行不论什么基于此线程的编码。比方,不要在终节器中訪问线程本地存储器。
      
终结器队列和终结器可达队列的交互是非常有趣的。Freachable中的f代表着Finalization终结器,reachable意思是对象能够被訪问。终结器可达队列被看作类似于全局变量和静态变量一样的根。因此,GC会认定终结器可达队列中全部对象都不是垃圾。

      
总而言之,当一个带有终结器的对象不可以被訪问到,GC觉得它是垃圾。然后,GC会移动此对象在终结器队列中的指针到终结器可达队列中,这时此对象将不再是垃圾,同一时候它所占的内存也不会被回收。到此为止,GC已经完毕了一次垃圾扫描识别过程。GC压缩可释放内存,那个特殊执行时线程进行清理终结器可达队列并执行每个对象的终结器。


GC进行二次垃圾回收时,带有终结器的垃圾对象就变成真正的垃圾,由于程序根不指向它,终结器可达队列也不指向它。这时这个对象所占的内存才会被回收。这里我要指出的点就是带有终结器的对象,GC须要对它们进行两次的垃圾回收才干回收它们所占的内存。实际上,GC有时须要运行两次以上的垃圾回收由于对象可能会被提升到更老的一代中。下图显示了GC二次回收后的托管堆。

GC二次回收后的托管堆:
技术分享



总结

终结器与GC的关系,我们须要掌握并理解。这有助于更深层次的理解垃圾回收GC机制。当然,还有比终结器很多其它的内容,下一节我们将介绍《复活与强制回收》。




郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。