zoukankan      html  css  js  c++  java
  • .Net 垃圾回收机制整理

    .Net CLR 的垃圾回收机制可以让开发者不必去追踪内存的使用,什么时候去回收内存。但是如果你想了解内存回收是如何工作的,本文会一步一步带你了解.net 的垃圾回收机制。 本文总结了Jeffrey Richter的Garbage Collection: Automatic Memory Management in the Microsoft .NET ,以及MSDN上的官方文档,整理如下。

         

    一 NextObjPtr

        当一个进程初始化的时候,runtime会保留一个连续的地址空间,对于.Net来说,这个练习的地址空间就是托管堆(Manged heap),托管堆会维护一个指针,我们叫它NextObjPtr,这个指针用来表示,下一个托管堆中的对象会在哪里生成,当一个对象的构造方法被调用后,new 运算符就会返回这个对象的地址。

    Figure1 表示一个托管堆中有三个对象,A,B,和C。那么下一个生成的对象就会在NextObjPtr处。但是当运行时去new一个对象时,可能会出现托管堆的内存不足的情况,这样就需要进行垃圾回收,需要一种机制来保证托管堆的空间一直是充足的。

    二 垃圾回收机制的算法

        GC会检查是否有某些对象,不再被使用。如有有这样的对象,那就意味着这些对象需要被回收,(如果托管堆中没有空间可以使用,CLR会抛出OutOfMemoryException),但是GC是怎么知道某个对象是否不再被引用的呢?

        每一个应用程序(application)都会引用一些列的根(Root),根标识了在托管堆上的地址。例如,所有全局的,以及static变量,线程栈上的参数,变量,寄存器包含一些托管对象的引用这些都被认为是应用程序root集合的一部分,这些root集合使用JIT 和CLR来维护的,并且运行GC访问。

       当GC开始运行你的生活,它会首先认为托管堆中所有的对象都是垃圾,然后遍历root集合,建立一个可访问对象的列表。

       Figure2显示了一系列的对象,A,C,D,F是直接被应用程序Root应用的,当遍历到D的时候,GC会发现H被D所使用,因此A,C,D,H都被加入到一个可访问对象的集合中,GC会递归遍历所有对象,找到所有的可访问对象。

       这个集合完成了一部分之后,GC会检查下一个Root,然后递归遍历这个root所引用的对象,当GC发现某个对象已经被加到集合中,就不会沿着这条路继续遍历。这样主要基于两点考虑:不去重复的遍历某个对象可以提高性能,也可以避免无穷递归。

       一旦所有的应用程序的root都被遍历过之后,GC就获得了一个可以被访问对象的列表。不在这个列表中的对象就应该考虑被回收。GC现在会一直遍历托管堆,寻找垃圾对象(垃圾对象占用的内存区现在被认为是可用的内存),把托管堆中的对象依次下移,填满垃圾对象占用的内存区,删掉托管堆上的内存空白区。同时GC需要修改Root集合里面的指向地址,修改NextObjPtr,GC需要保证他们指的地址是正确的。Figure3显示了GC回收之后的托管堆。

        既然GC的功能如此强大,为什么ANSI C++里面没有实现这样的功能呢?原因是程序的Root集必须能识别所有的Root,并且能够找到Root对应的对象,C++可以把一个类型的指针强转为另外一个类型的,因此没有办法知道指针真正指向的是一个什么对象。在CLR中,托管堆知道一个对象的真正类型,因为托管对象的metadata记录了这些信息。

    三 Finalization和析构函数

        GC提供了另外一个你可能会利用到的功能:Finalization。Finalization可以优雅的实现在GC回收托管资源之后清理自己占用的其它资源。通过Finalization,在GC决定释放资源的时候,对文件资源,网络连接资源等进行自我清理。一个简单的例子:

    class Car
        {
            ~Car()
            {
                // destructor            
                // cleanup statements...
                Console.WriteLine("In Finalize.");
            }
        }

        编译之后,析构函数会隐式的调用Finalize方法,析构函代码就会变成了下面的样子,你不能直接重写Finalize方法,只能通过实现析构函数语法来实现Finalize的功能

    protected override void Finalize()
            {
                try
                {        // Cleanup statements...
    
                    Console.WriteLine("In Finalize.");
                }
                finally
                {
                    base.Finalize();
                }
            }
      你可以这样创建一个新对象
    Car car = new Car();
                     在某一个时间,当GC回收这个对象的时候,发现这个类实现了Finalize方法,就会调用它,因此"In Finalize"会在控制台上显示出来,这个对象占用的资源会被回收。当设计一个类的时候,尽量避免使用Finalize方法,有以下几点理由:
    • Finalizable对象会被提升到旧的Generation中。这会提高内存的压力,并且组织GC对资源的释放。另外,所有直接或者间接的被Finalizable对象引用的资源也都会被提升Generation,会在后面的文章中介绍到。
    • 您应该只实现 Finalize 方法来清理非托管资源。您不应该对托管对象实现 Finalize 方法,因为垃圾回收器会自动清理托管资源
    • 强制GC调用Finalize方法会降低性能。记住,如果每个对象都是Finalizable,10000个对象就需要调用每个对象的Finalize方法。
    • Finalizable对象可能会引用其他非Finalizable对象,没有必要的延长的被引用对象的生命周期。这种情况下,你可以考虑把这个类分成两个不同的类,一个轻量级的类实现Finalize方法,另外一个没有Finalize方法的类引用其他类。
    • 你没有办法控制什么时候Finalize方法会被执行。对象可能会一直占用资源,直到GC下次运行。
    • 当一个程序停止时,一些对象仍然是可访问的,因此他的Finalize方法可能还没有被调用。这种情况可能会发生在后台线程在使用某个对象,对象是在英语程序关闭的过程中创建的,或者appDomain正在卸载。默认情况下,应用程序正在关闭的过程中,不可访问的对象的的Finalize方法不会被调用,程序会很快的终止。当然,操作系统会回收资源,但是托管堆里的资源不会被优雅的释放。你可以通过调用GC.RequestFinalizeOnShutdown来改变这个默认行为。当然你要小心的使用这个方法,因为你在改变整个程序的设置。
    • 运行时无法保证Finalize方法的调用顺序。例如,一个对象含有一个内部对象的引用。GC检测到两个对象都需要被回收。而且,内部对象的Finalize方法应该先被调用。现在,外部对象的Finalize方法是允许访问内部对象的方法的,并且调用了内部对象的方法,这就会导致不可预料的错误。因此,强烈建议Finalize方法不用引用任何内部成员的对象。
    • 当你决定为一个类定义Finalize方法的时候,确保Finalize方法不会抛出异常,这个异常是无法被捕获的,会导致应用程序终止。

    四 Finalization的内部实现

       表面上看,finalization的实现是很干脆的。你创建一个对象,当对象被回收的时候,对象的Finalize方法被调用。实际上比这个要复杂。

       当一个程序创建一个新的对象,new操作在托管堆上分配内存,如果这个类定义了Finalize方法,一个指向这个对象的指针会加到一个Finalization队列中,Finalization队列是由GC维护的内部的数据结构,队列中每一个成员都必须实现了Finalize方法,并且保证Finalize方法在资源回收之前被调用。

       Figure5显示了一个堆包含了一些对象。这里面有些对象可以在应用程序root集合中访问,有些不能。当C,E,F,I 和 J被创建的时候,系统会检测实现了Finalize方法的对象,加到Finalize队列中。

       当GC开始回收的时候,B,E,G,H 和 J决定要回收。GC会检测Finalization队列中是否存在这些对象的引用。如果存在,则把这个对象从Finalization队列中移除,加到Freachable队列中。这个Freachable队列是GC维护的另外一个数据结构。Freachable队列中表示准备调用这些对象的Finalize方法。

        在回收之后,托管堆就像Figure6。你可以看到B,G,H占用的内存已经被回收了,因为这些对象没有实现Finalize方法。然而,被E,I,J占用的内存无法被回收,因为他们的Finalize方法还没有被调用。

        有一个特别的运行时线程专门负责调用Finalize方法,当Freachable队列是空的时候(大多数情况都是这样),这个线程处于sleep状态。当里面有内容的时候,线程被唤醒,逐个把队列中的内容删除,并且调用每一个对象的Finalize方法。因此,Finalize方法中不要假设方法在某个线程中执行,也不要在Finalize方法中只有当前线程才能访问的成员。

        这里简单说一下Freachable的命名,F当然就是finalization的意思,Freachable队列也被认为是Application Root集合的一部分,和全局的变量或者静态变量一样,因此在Freachable队列中的对象是可访问的,不属于垃圾对象。

        简单的说,当一个对象是不可访问的时候,GC会认为这个对象是垃圾对象。然后GC会把对象从Finalization队列中移到Freachable队列中,这时这个对象就不再被认为是垃圾对象,因此他的资源也不会被回收。因此,曾经被GC认为是垃圾的对象会被重新归类成非垃圾对象。GC会重新安排可回收的资源并且等待特定的运行时线程清空Freachable队列,执行每一个对象的Finalize方法。

      

        GC下一次运行的时候,所有的Finalized对象会被认为是真正的垃圾,因为没有任何Root指向Freachable队列了。现在这些垃圾对象占用的内存会被回收了。这里需要注意,实现了finalization的对象需要GC运行至少两次才能被回收。事实上,可能多余两次,因为这些对象可能被提升到更旧的Generation中。Figure7中显示了GC第二次回收之后堆栈的情况。

    五 复活

        Finalization的完整概念令人着迷的,但是我们还有更多需要说的。在之前的段落中你可能会注意到,当应用程序不能访问一个生存的对象时,我们会认为这个对象已经死亡。但是,如果这个对象需要Finalization,这个对象的又被认为活着的,直到他的Finalize方法被调用,之后他才是真正的死亡了。换句话说,需要Finalize对象的生命周期会经历一个从死亡,生存,真正死亡的过程。这种情况我们叫做resurrection,正如他的名字暗示的一样,这个对象经历了一个复活的过程。

    public class BaseObj
        {
            protected override void Finalize()
            {
                Application.ObjHolder = this;
            }
        }
        class Application
        {
            static public Object ObjHolder; // Defaults to null
        }

        在这个例子中,当一个对象的Finalize方法被执行的时候,Application类引用了当前对象,这个对象又复活了,但是实际上这个对象的Finalize方法已经被执行过了,这可能会导致无法预期的结果。因此记住,Finalize方法中引用的所有对象都会复活,在Finalize方法中当前对象被其他对象引用可能会导致不可预期的异常,因为这个对象已经被Finalize了。

    事实上,当设计一个类的时候,复活的对象可能会超出你的控制。对象复活很少有漂亮的用法,因此我们应该尽量的去避免它。

        当你确认要使用Finalize的时候,你可以通过一个bool变量来控制这个对象是否被Finalized,.Net FrameWork中很多类库给出了一个优雅的实现,实现的基本代码是这样的:

    class ClassNeedFinalize : IDisposable
        {
            private bool isDispose;
            public void Dispose()
            {
                Dispose(true);
                System.GC.SuppressFinalize(this);//通知GC不再需要调用这个类的Finalize方法
            }
    
            protected virtual void Dispose(bool disposing)
            {
                if (!isDispose)
                {
                    if (disposing)
                    {
                        //释放托管资源
                    }
                    //释放非托管资源
                }
                isDispose = true;
            }
            protected override void Finalize()
            {
                Dispose(false);
            }
            public void SomeMethod()
            {
                if (isDispose)
                {
                    throw new Exception();
                }
            }
    
        }

    六 垃圾回收的时机

    当满足以下条件之一时将发生垃圾回收:

    系统具有低的物理内存。

    由托管堆上已分配的对象使用的内存超出了可接受的阈值。这意味着可接受的内存使用的阈值已超过托管堆。随着进程的运行,此阈值会不断地进行调整。

    调用 GC.Collect 方法。几乎在所有情况下,您都不必调用此方法,因为垃圾回收器会持续运行。此方法主要用于特殊情况和测试。

    参考文献:

    Garbage Collection: Automatic Memory Management in the Microsoft .NET Jeffrey Richter

    msdn 官方文档

  • 相关阅读:
    再次或多次格式化导致namenode的ClusterID和datanode的ClusterID之间不一致的问题解决办法
    Linux安装aria2
    POJ 3335 Rotating Scoreboard 半平面交
    hdu 1540 Tunnel Warfare 线段树 区间合并
    hdu 3397 Sequence operation 线段树 区间更新 区间合并
    hud 3308 LCIS 线段树 区间合并
    POJ 3667 Hotel 线段树 区间合并
    POJ 2528 Mayor's posters 贴海报 线段树 区间更新
    POJ 2299 Ultra-QuickSort 求逆序数 线段树或树状数组 离散化
    POJ 3468 A Simple Problem with Integers 线段树成段更新
  • 原文地址:https://www.cnblogs.com/myprogram/p/2842640.html
Copyright © 2011-2022 走看看