.Net CLR 的垃圾回收机制可以让开发者不必去追踪内存的使用,什么时候去回收内存。但是如果你想了解内存回收是如何工作的,本文会一步一步带你了解.net 的垃圾回收机制。 本文总结了Jeffrey Richter的Garbage Collection: Automatic Memory Management in the Microsoft .NET ,以及MSDN上的官方文档,整理如下。
当一个进程初始化的时候,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发现某个对象已经被加到集合中,就不会沿着这条路继续遍历。这样主要基于两点考虑:不去重复的遍历某个对象可以提高性能,也可以避免无穷递归。
既然GC的功能如此强大,为什么ANSI C++里面没有实现这样的功能呢?原因是程序的Root集必须能识别所有的Root,并且能够找到Root对应的对象,C++可以把一个类型的指针强转为另外一个类型的,因此没有办法知道指针真正指向的是一个什么对象。在CLR中,托管堆知道一个对象的真正类型,因为托管对象的metadata记录了这些信息。
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();
表面上看,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