每个.Net应用程序都有一组根(root)。一个根是一个存储位置,其中包含着一个指向引用类型的内存指针。该指针或指向一个托管堆中的对象,或被设为null。例如,所有的全局引用类型变量或静态引用类型变量都被认为是根。另外,一个线程堆栈上所有引用类型的本地变量或参数变量也被认为是一个根。最后,在一个方法内,指向引用类型对象的CPU寄存器也是一个根。

  当JIT编译器编译一个方法的IL代码时,除了产生本地CPU代码外,JIT编译器还创建一个内部的表。该表中的每一个条目都标识着一个方法的本地CPU指令的字节偏移范围,以及该范围中一组包含根的内存地址(或CPU寄存器)。如下图:

  如果在0x00000021和ox00000122间的代码执行时开始了垃圾收集,那么垃圾收集器将知道参数this、参数args2,本地变量fs及寄存器EBX都是根,它们引用的托管堆中的对象将不会被认为是可收集的垃圾对象。垃圾收集器还可遍历线程的调用堆栈,通过检测其中每一个方法的内部表来确定所有调用方法中的根。最后,垃圾收集器使用其他一些手段来获得全局引用类型变量和静态引用类型变量中的根。

  在上图中,方法的arg1参数在偏移为0x00000020处的指令执行完毕后就不再被引用了。这意味着arg1引用的对象在该指令执行后的任何时刻都可以被执行垃圾收集(假设没有其他的根再引用该对象)。换句话说,只要一个对象不再可达,它就是垃圾收集的候选对象。

  当垃圾收集器开始执行时,它假设托管堆中的所有对象都是可收集的垃圾。然后,垃圾收集器以递归方式遍历所有的根,构造出一个包含所有可达对象的图。如下图示:

  垃圾收集器一旦检查完所有的根,其得到的可达对象图将包含所有从应用程序的根可以访问的对象。任何不在该图中的对象都将是应用程序的不可访问对象,因此也是可以被执行垃圾收集的对象。垃圾收集器接着线性的遍历托管堆以寻找包含可收集垃圾对象的连续区块(这些区块现在被认为是自由空间)。一些容量较小的内存块将被忽略不计。

  如果找到了较大的连续区块,垃圾收集器会把内存中的一些非垃圾对象搬移到这些连续区块中(使用memcpy函数)以压缩托管堆。同时,垃圾收集器必须修改应用程序的根,以使它们指向这些对象更新后的位置。另外,如果任何对象包含指向这些对象的指针,那么垃圾收集器也会负责矫正它们。在托管堆中的内存被压缩后,托管堆上的NextObjPtr指针将被设为指向最后一个非垃圾对象之后。如下图:

  如我们所见,垃圾收集会给应用程序带来不小的性能损伤,这也是使用托管堆时主要的负面影响。但是,我们要清楚垃圾收集只有在第0代对象充满时才会出现。在这之前,托管堆的性能要比C语言运行时中的堆的性能高许多。

  问题:既然垃圾收集的功能如此强大,那它为什么没有被ANSI C++所采用呢?

  这是因为垃圾收集器必须能够识别出应用程序的根,并且还要找到所有的对象指针。非托管C++的问题在于它允许我们将一个指针从一个类型转换为另一个类型,而我们无从知道它真正引用的对象是什么。但在CLR中,托管堆总能知道一个对象的实际类型,从而使用其元数据信息来判断一个对象的哪些成员引用着其他的对象。

posted on 2011-03-30 21:30  辛勤的代码工  阅读(324)  评论(0编辑  收藏  举报