前面一篇文章介绍了垃圾回收的基本工做原理,垃圾回收器并非能够管理内存中的全部资源。对于全部的托管资源都将有.NET垃圾回收机制来释放,可是,对于一些非托管资源,咱们就须要本身编写代码来清理这类资源了。程序员
其实在C#开发中,大部分资源均可以经过.NET垃圾回收机制进行回收,只用当咱们使用非托管资源(原始的操做系统文件句柄,原始的非托管数据库链接,非托管内存等等)的时候,咱们才须要实现本身的资源清理代码。数据库
.NET提供了两种释放非托管资源的方式,类型本身的Finalize方法和IDisposable接口的Dispose方法。app
下面就来看看这两个跟垃圾回收相关的方法。ide
在.NET的基类System.Object中,定义了名为Finalize()的虚方法,这个方法默认什么都不作。函数
咱们能够为自定义的类型重写Finalize方法,在该方法中加入必要的非托管资源清理逻辑。当要从内存中删除这个类型的对象时,垃圾回收器会调用对象的Finalize方法。因此,不管.NET进行一次自发的垃圾回收,仍是咱们经过GC.Collect()进行强制垃圾回收,Finalize方法老是会被调用。另外,当承载应用程序的AppDomain从内存中移除时,一样会调用Finalize方法。性能
假设咱们如今有一个使用非托管资源的类型,那么咱们就须要重写Finalize方法来进行非托管资源的清理,可是当经过下面的方式重写Finalize方法的时候,咱们会获得一个编译错误。this
class MyResourceWrapper { protected override void Finalize() { } }
其实,当咱们想要重写Finalize方法时,C#为咱们提供了(相似C++)析构函数语法(C#终结器)来重写该方法。C#终结器和构造函数语法相似,方法名称都和类型名称同样;不一样的是,终结器具备~前缀,而且不能使用访问修饰符,不接受参数,也不能重载,因此一个类只能有一个终结器。spa
class MyResourceWrapper { ~MyResourceWrapper() { Console.WriteLine("release unmanaged resources"); Console.Beep(); } }
之因此C#只支持这种方式进行Finalize方法的重写,是由于C#编译器会为Finalize方法隐式地加入一些必需的基础代码。下面就是咱们经过ILSpy查看到了IL代码,Finalize方法做用域内的代码被放在了一个try块中,而后无论在try块中是否遇到异常,finally块保证了Finalize方法老是可以被执行。操作系统
.method family hidebysig virtual instance void Finalize () cil managed { // Method begins at RVA 0x2050 // Code size 31 (0x1f) .maxstack 1 .try { IL_0000: nop IL_0001: ldstr "release unmanaged resources" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: call void [mscorlib]System.Console::Beep() IL_0011: nop IL_0012: nop IL_0013: leave.s IL_001d } // end .try finally { IL_0015: ldarg.0 IL_0016: call instance void [mscorlib]System.Object::Finalize() IL_001b: nop IL_001c: endfinally } // end handler IL_001d: nop IL_001e: ret } // end of method MyResourceWrapper::Finalize
当咱们执行下面代码时,咱们就能够听到系统蜂鸣声,像咱们前面介绍的同样AppDomain被移除内存,类型终结器将被调用。线程
static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); }
Finalize的工做机制仍是比较复杂的,这里只是简单的介绍,更多的原理你们能够本身网上查查。
当在托管堆上分配对象空间时,运行库会自动肯定该对象是否提供一个自定义的Finalize方法。若是是这样,对象被标记为可终结的,同时一个指向这个对象的指针被保存在名为终结队列的内部队列中。终结队列是一个由垃圾回收器维护的表,它指向每个在从堆上删除以前必须终结的对象。
当垃圾回收器肯定到了从内存中释放一个对象的时间时,它检查终结队列上的每个项,并将对象从堆上复制到另外一个称做终结可达表(finalization reachable table的托管结构上。此时,下一个垃圾回收时将产生另一个线程,为每个在可达表中的对象调用Finalize方法。所以,为了真正终结一个对象,至少要进行两次垃圾回收。
从上面能够看到,Finalize方法的调用是至关消耗资源的。Finalize方法的做用是保证.NET对象可以在垃圾回收时清理非托管资源,若是建立了一个不使用非托管资源的类型,实现终结器是没有任何做用的。因此说,若是没有特殊的需求应该避免重写Finalize方法。
当垃圾回收生效时,能够利用终结器来释放非托管资源。然而,不少非托管资源都很是宝贵(如数据库和文件句柄),因此它们应该尽量快的被清除,而不能依靠垃圾回收的发生。除了重写Finalize以外,类还能够实现IDisposable接口,而后在代码中主动调用Dispose方法来释放资源。
看一个例子:
class MyResourceWrapper:IDisposable { public void Dispose() { Console.WriteLine("release resources with Dispose"); Console.Beep(); } } class Program { static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); mr.Dispose(); } }
一样,当咱们显示的调用Dispose方法的时候,能够听到系统的蜂鸣声。
注意,经过Dispose进行资源的释放也是有潜在的风险的,由于Dispose方法须要被程序员显示的调用,若是代码中漏掉了Dispose的调用或者在Dispose调用以前产生了异常从而没有指定Dispose,那么有些资源可能就一直留在内存中了。
因此咱们应该使用下面的方式保证Dispose方法能够被调用到:
static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); try { //do something wiht mr object } finally { mr.Dispose(); } }
可是,每次编写Dispose的代码都使用try块会以为很麻烦,还好C#中,咱们能够重用using关键字来简化Dispose的调用。
在C#中,using语句提供了一个高效的调用对象Dispose方法的方式。对于任何IDispose接口的类型,均可以使用using语句,而对于那些没有实现IDisposable接口的类型,使用using语句会致使一个编译错误。
static void Main(string[] args) { using (MyResourceWrapper mr = new MyResourceWrapper()) { //do something with mr object } }
在using语句块结束的时候,mr实例的Dispose方法将会被自动调用。using语句不只免除了程序员输入Dispose调用的代码,它还保证Dispose方法被调用,不管using语句块顺利执行结束,仍是抛出一个异常。事实上,C#编译器为using语句自动添加了try/finally块。咱们能够看看using的IL代码:
.try { IL_0007: nop IL_0008: nop IL_0009: leave.s IL_001b } // end .try finally { IL_000b: ldloc.0 IL_000c: ldnull IL_000d: ceq IL_000f: stloc.1 IL_0010: ldloc.1 IL_0011: brtrue.s IL_001a IL_0013: ldloc.0 IL_0014: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0019: nop IL_001a: endfinally } // end handler
从前面的介绍了解到,Finalize能够经过垃圾回收进行自动的调用,而Dispose须要被代码显示的调用,因此,为了保险起见,对于一些非托管资源,仍是有必要实现终结器的。也就是说,若是咱们忘记了显示的调用Dispose,那么垃圾回收也会调用Finalize,从而保证非托管资源的回收。
其实,MSDN上给咱们提供了一种很好的模式来实现IDisposable接口来结合Dispose和Finalize,例以下面的代码:
class MyResourceWrapper:IDisposable { private bool IsDisposed=false; public void Dispose() { Dispose(true); //tell GC not invoke Finalize method GC.SuppressFinalize(this); } protected void Dispose(bool Disposing) { if(!IsDisposed) { if(Disposing) { //clear managed resources } //clear unmanaged resources } IsDisposed=true; } ~MyResourceWrapper() { Dispose(false); } }
在这个模式中,void Dispose(bool Disposing)函数经过一个Disposing参数来区别当前是不是被Dispose()调用。若是是被Dispose()调用,那么须要同时释放托管和非托管的资源。若是是被终结器调用了,那么只须要释放非托管的资源便可。Dispose()函数是被其它代码显式调用并要求释放资源的,而Finalize是被GC调用的。
另外,因为在Dispose()中已经释放了托管和非托管的资源,所以在对象被GC回收时再次调用Finalize是没有必要的,因此在Dispose()中调用GC.SuppressFinalize(this)避免重复调用Finalize。一样,由于IsDisposed变量的存在,资源只会被释放一次,多余的调用会被忽略。
因此这个模式的优势能够总结为:
本文介绍了.NET垃圾回收中两个相关的方法:Dispose和Finalize。Finalize的目的是用于释放非托管的资源,而Dispose是用于释放全部资源,包括托管的和非托管的。
Dispose须要在代码中进行显示的调用,而Finalize则是由垃圾回收自动调用,为了更有效的结合Dispose和Finalize,文中还介绍了MSDN中给出的实现IDisposable接口的一个模式。