Garbage Collection Essence--.Net中Garbage Collection深入探讨
keywords:
memory leak内存泄漏,managed heap托管堆,reference对象引用,Generational Garbage Collector分代的垃圾回收器,finalizer终结器,finalize终结方法,Dispose处置,managed resource& unmanaged resource托管资源&非托管资源
正文:
part I:managed resources的回收
在.Net Framework里面,当我们通过new 创建某个对象时,其会在在managed heap上开辟相应的空间,同时返回一个指向该对象的reference,不能混淆的是reference 是位于非托管的stack上的,只是该reference指向位于managed heap上的对象。当对象不可达(unreachable)的时,garbage collector将将其回收。这就是.Net Framework的垃圾自动回收机制。
因此,在.net framework里面,一般情况下,不需要我们自己编写任何内存回收的代码,来进行垃圾回收的处理。
例如,我们创建一个MyClass类型的instance,
MyClass obj=new MyClass();
当将其compile后,我们利用ILDASM可以看到,其相应的CIL代码如下:
.....
IL_0000: newobj instance void .MyClass::.ctor()
......
实际上,newobj指令通知CLR执行下面的核心任务:
1 计算分配对象所需要的总内存数(包含类型的成员变量和类型的基类所需的必要内存)
2 检查托管堆,确保有足够的空间来放置要分配的对象。如果空间足够,调用类型的构造函数,最终将内存中新对象的reference返回给调用者。
3 最后,在将引用返回给调用者之前,移动下一个对象的指针,指向managed heap上的下一个可用的位置。
Regulation 1:如果托管堆没有足够的内存来分配所请求的对象,就会启动GC进程进行垃圾回收。
当确认发生回收时,GC会暂时挂起所有当前processes中的threads,以保证在垃圾回收过程中不会访问managed heap。一旦回收结束,挂起的thread继续他们的工作。.NET GC已经作了精心优化,所以用户很少感觉到应用程序的中断。
那么,如何确定哪些对象不再被引用而成为GC回收的对象呢?
在此,我们引入Application Root(应用程序根)的概念。
所谓Application Root,可以看成是一种记录所有managed heap上对象的引用的一种单元。也就是说,凡是被引用的对象,在Application root上都有其相应的记录,该对象称为有根的(rooted), 否则被标为终结(finalize),就是可以回收的垃圾了。当garbage回收之后,Application Root修改相应的集合,managed heap指针作进一步调整指向managed heap最后对象的下一个地址。
垃圾回收器的分代(generational garbage collector)
当CLR试图寻找unreachable的对象时,如果逐个全部检查managed heap里面的所有对象时,将花费大量精力。那么Micorsoft设计了一种可分代的垃圾回收器。 他的基本假设是:如果某个对象某次不会回收,那么他下次不会被回收的可能性较大,也即对象在heap上停留的时间越长,他就跟可能被保留。相反,最近才放到managed heap上的对象可能很快就unreachable了,比如一些临时对象。
基于以上架设(题外话:这个假设就是virtual memory的实现可能性的假设,是一样的),MS把managed heap分为3代managed heap:即第0代,第1代和第2代。在回收过程中,始终回收第0代的garbage。凡是没有回收的第0代中的对象被装移到第1代managed heap中;以此类推。
在.Net FrameWork里面,所有有关垃圾回收的namespace都在system.GC里面。一般来说,我们不需要explicitly 调用GC.Collect来进行垃圾回收。只有在创建了那些使用了unmanaged resources的类型时,才需要我们explicitly调用相应的垃圾回收方法。
part II:unmanaged resources 的回收
我们知道,大多数的C#类都不需要显示的回收垃圾处理,所有这些对象都会被GC回收。只有在我们使用unmanaged resources, 才可能需要设计一个在用完后清理自身的类。这是涉及到的概念就有3个,分别是析构函数,Finalize(),和Dispose()。下面分别一一讲述。
1.Finalize()
Finalize()是System.Object类的一个虚方法
public class Object
{
.....
protectd virtual void Finalize(){}
}
当用户在自定义的class中override了Finalize()时候,就为该类型重新定义了清理逻辑。但是,该方法是protected,所以在自定义类中不可能explicitly调用Finalize()。那么我们自定义的Finalize()如何被调用的呢?实际上,都是由GC在从内存中clean 对象之前,GC会自动调用对象的Finalize()方法(如果override了的话)。具体过程相当complex :)
题外话:
问:如何override自定义类的Finalize()?
答:按理说,在自定义类中,我们可以protected overide 来自Object类的Finalize()方法。可是奇怪的是,C#中部允许override这个Finalize()呢?我们只能通过析构函数来override Finalize().
问:GC是如何回收explicitly实现了Finalize()的对象的managed heap上的空间?
答:
当在managed heap上分配对象时,CLR自动确定该对象是否提供一个自定义的Finalize()方法。如果是这样,对象被标记为可终结的, 这时一个指向该对象的指针被保存在终结队列(Finalization List)的内部列表中。终结队列是由GC维护的表,他指向每一个在managed heap上clean之前必须被终结的对象。
当GC确定开始回收时,他检查Finalization List上的每一个项,并将对象从managed heap上复制到另一个称为终结可达表(Finalization reachable table)的托管结构上。此时,下一个垃圾回收的时候产生另外一个线程,为每一个在可达表中的对象调用Finalize()方法。
因此,为了真正终结一个对象,至少需要进行2次垃圾回收。
因此,只要有可能的话,应该在设计的时候尽量避免override Finalize(),除非是使用了unmanaged resource。否则会有很大代价也没必要。
2 析构函数
大家在C++里面可能对于析构函数非常了解,但是在C#里面绝少有析构函数的说法。因为一般情况下,我们根本不需要自己处理资源回收。那么,在C#里面,到底有没有析构函数呢?
应该说,在C#里面,我们可以编写析构函数,但是其本质是:对象的析构函数在编译后将会编译成为对象的Finalize()方法.只不过,在Finalize方法中,增加了许多必需的基础代码。也就是说,实际上还是回到了Finalize()的老路上去了。
3 Dispose()
除了以上通过override Finalize()这个方法可以implement对对unmanaged resource进行清理外,我们还可以通过实现IDisposable接口的Dispose()方法,来实现对unmanaged resource的release。因此,如果我们自定义的类中实现了IDisposable的Dispose方法。那么在对象不再被引用时,应该手动调用对象的Dispose()方法。这样对象可以执行必要的unmanaged resource的release,而不需要将对象放到Finalization List上导致的系统开销。
题外话:
问:struct和class都可以override IDispoable的Dispose()方法吗?同样,Finalize()方法呢?
答:struct和class 都可以实现 IDisposable的Dispose()方法,但是struct不能override Finalize()!因为struct是 value type,根本不存在析构函数!
继续刚才的话题,我们我们implement IDisposable 的Dispose()方法后,要实现对unmanaged resource的release, 对象必须保证最后无论如何显示调用Dispose()方法,所以最好就是try-catch-finally了。这样相当麻烦。为了解决这一问题,C#提供了using 这一语法。例如
using(MyClass obj=new MyClass())
{
.....
}
当退出using 的作用域时,其会自动调用obj.Dispose()方法!so cool :)!因此,当引用using 指令时,其对象必须保证显示实现了IDisposable的Dispose方法;否则编译错误!!
(using 还可以表示对namespace的引用,不要搞错了!)
写到这里,我想读者对.Net Framework的垃圾回收及实现机制应该差不多清楚了。