C#基础知识梳理系列十二:终结操作及资源清理
经过上一章的讨论,我们知道,CLR会在必要的时候启动垃圾回收器对不再使用的对象所占的内存进行回收,其实,在一个对象被回收前我们还可以通过析构函数来实现终结操作释放资源,了解终结操作后,我们还可以使用Dispose模式进行手工强制清理资源。这一章我们将讨论这些相关话题。
C#与C++有着类似的析构函数,都是对资源进行清理,但是,在C++中,开发人员明确知道析构函数会被调用,而C#中,开发人员不太明确析构函数会在什么时候被调用,它是由CLR管理的,通常是在一个对象被标记为垃圾对象,如果有析构函数,CLR的垃圾回收器会先调用析构函数,然后再回收其内存。
类型System.Object有一个受保护的虚方法protected virtual void Finalize();这个就是“析构函数”。如果想为一个类型添加析构函数,必须使用与C++类型的语法结构:前置波浪线+类名,相当于无参构造函数的名前加上波浪线,如下:
public class Code_12 : IApp { public void DoWork() { } ~Code_12() { Console.WriteLine("Clear Code_12"); } }
析构函数前不能有任何访问修饰符,并且一个类型只能有一个析构函数。编译后,上面的~Code_12()被编译成名为Finalize的方法,如下图:
可以看到,编译过程实际上是对基类Object的虚方法Finalize()的重写,可以非常强悍地认为Finalize就是析构函数~Code_12()的别名,二者只是书写方式不同,干的都是一样的擦屁股的活。我们再来看一下它的内部IL:
.method family hidebysig virtual instance void Finalize() cil managed { // 代码大小 25 (0x19) .maxstack 1 .try { IL_0000: nop IL_0001: ldstr "Clear Code_12" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: nop IL_000d: leave.s IL_0017 } // end .try finally { IL_000f: ldarg.0 IL_0010: call instance void [mscorlib]System.Object::Finalize() IL_0015: nop IL_0016: endfinally } // end handler IL_0017: nop IL_0018: ret } // end of method Code_12::Finalize
可以看到,Finalize()方法内实际上是将代码包装到try…finally块内,我们实现的代码被放到了try块,在finally块内调用了基类的Finalize方法,相当于base.Finalize()。
前面我们说过,析构函数是在垃圾回收器回收垃圾对象之前的最后才执行一些清理工作,它的执行是受CLR管理,非人工可控,我们通过一个示例代码来看一下它的执行顺序:
public class Code_12_01 { public Code_12_01() { Console.WriteLine("Create Code_12_01"); } ~Code_12_01() { Console.WriteLine("Clear Code_12_01"); } } public class Code_12_02 : Code_12_01 { public Code_12_02() { Console.WriteLine("Create Code_12_02"); } ~Code_12_02() { Console.WriteLine("Clear Code_12_02"); } } public class Code_12_03 : Code_12_02 { public Code_12_03() { Console.WriteLine("Create Code_12_03"); } ~Code_12_03() { Console.WriteLine("Clear Code_12_03"); } }
执行以下代码,先创建,再遗弃,最后垃圾回收:
public void DoWork() { Code_12_03 temp = new Code_12_03(); temp = null; //遗弃对象,等待垃圾回收 GC.Collect(); }
打印结果:
Create Code_12_01
Create Code_12_02
Create Code_12_03
Clear Code_12_03
Clear Code_12_02
Clear Code_12_01
在调用析构函数过程中,是从派生类逐级向上调用基类的析构函数。
CLR对这种在垃圾回收前调用对象的析构函数进行资源清理的工作就是终结操作。我们不能控制垃圾回收,同样也不能控制终结操作,调用GC.Collect();只是向CLR发出垃圾回收的请求,垃圾回收器对一第列对象回收的先后顺序,我们是无法控制的。
在我们平时开发工作中,或多或少都会用到本地资源,如打开文件,网络连接等,对这些资源的操作实际上是CLR通过Windows获取资源的独占句柄,使用完后,必须释放句柄,否则其他访问者将无法使用,例如我们经常碰到的异常“文件XXX正常由另一进程使用,因此该进程无法访问该文件。”就是这种原因造成的。
终结操作就是利用析构函数来释放/清理资源,在对象被垃圾回收前,CLR调用Finalize()方法做清理工作,前提是我们提供了析构函数。这通常在我们定义类型中使用到本地资源的时候非常有用。如下一个文件管理类:
public class FileManager { FileStream fs = null; public FileManager() { } ~FileManager() { if (fs != null) { fs.Close(); } } }
在调用析构函数中,应该确保其内部不该出异常,其实前一节中~Code_12()的IL代码也可以看出来,并没有与try对应的catch块。所以我们在~FileManager()内对fs进行了判空。垃圾回收器发现FileManager对象不再可用时,会调用Finalize(),在内部关闭fs,接着就是回收其内存了。通常在有垃圾回收的时候都有可能调用Finalize()方法。
事实上,在实现了Finalize()的对象内存被回收过程并不是如此简单,这个终结操作有时可能须要执行两次或更多次垃圾回收才能达到释放其内存的目的,继续往下看。
先来看两个垃圾回收器管理的列表:
终结列表(Finalization List):放置所有实现了Finalize()方法的对象的指针。
Frachable队列:放置已被认定为垃圾对象且实现了Finalize()的对象的指针,这里的指针是从终结列表中移过来的。
如果类型实现了Finalize(),在创建该类型对象前,即调用构造器之前,CLR会将该对象的一个指针放到一个终结列表(Finalization List)中,终结列表是由垃圾回收器管理的一个数据结构。在一次垃圾回收前,垃圾回收器会标记所有的不可达对象,这时所有的不可达对象已经被判了“死刑”,接着扫描终结列表以查找实现了Finalize()方法的对象的指针,并将这些指针从终结列表中移到Frachable队列中,当Frachable队列不为空时,CLR有一个专门的线程来负责调用队列中指针对应对象的Finalize方法,为了能够调用这些对象的Finalize()方法,必须重新激活这些对象,也就是从“不可达”状态变成“可达”状态,这个过程是使对象复活的过程,调用完Finalize()方法后,该对象就彻底完蛋了,接着就是等待下一次垃圾回收时对其内存进行回收,它永远再无出头之日了(当然,如果在析构函数中把该对象的指针放到一个其他静态变量中,那情况就不一样了。有兴趣的可以自己测试一下。)。在这个过程中,假如对象已被标记为垃圾,但未调用其Finalize方法前,该对象可能被从第0代提升为第1代,则有可能要经过更多次垃圾回收才能释放其内存。
GC类提供了一个静态方法GC.WaitForPendingFinalizers(), 此方法将挂起当前线程,接着垃圾回收器清空Frachable队列并调用其中对象的Finalize()方法,全部调用完毕后被挂起的线程才得以恢复。前面的描述中我们知道,即使调用了Finalize()方法也不一定能立即释放该对象的内存,所以可以在GC.WaitForPendingFinalizers()方法后立即跟一个GC.Collect()进行回收,但微软不建议我们不这么干。
在上面的讨论中,我们知道Finalize方法是受CLR管控的,也就是一个类型的本地资源在什么时候得到清理,我们并不太清楚,有没有一种方法可以让我们对其方便地控制呢?当然有!那就是实现Dispose模式。
接口System.IDisposeable提供了对Dispose模式实现的最佳实践。
public interface IDisposable { void Dispose(); }
通过实现这个接口,我们可以方便规范地来管理包装了本地资源的类对象,这个模式起到了双重保险的作用,可以分别清理托管和非托管资源。像TextReader、FileStream、WinForm窗体都实现了这个接口,这里我们推荐MSDN上面的实现方式。我们继续对上面的FileManager类进行改造:
public class FileManager : IDisposable { private bool disposed = false; //非托管资源 private IntPtr handle; //托管资源 FileStream fs = null; public FileManager() { } ~FileManager() { //GC调用,终结 Dispose(false); } public void Dispose() { //显示关闭 Dispose(true); //通知GC不必再调用Finalize() GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing && fs != null) { // 释放托管资源 fs.Dispose(); } //释放非托管资源 //... handle = IntPtr.Zero; disposed = true; } } }
除了实现接口的Dispose()方法,还创建了一个通用的方法Dispose(bool disposing) ,在此方法内,如果对象已经被清理,则不再做清理工作。有两点要注意:
(1)Dispose()方法内部,传递了一个true值告诉Dispose(bool disposing)方法此次调用是手工调用,接着使用一个静态方法GC.SuppressFinalize(this);告诉垃圾回收器,此对象我已经手工调用清理资源的方法,你不必再调用Finalize()方法了。
(2)~FileManager()方法内是传一个false值告诉Dispose(bool disposing)此对象是通过垃圾回收器清理资源的。
也就是说,对于FileManager对象,如果你手工调用了清理的方法,则会对托管和非托管方法进行清理,如果你忘记了手工调用,垃圾回收器还是会通过调用Finalize()方法对非托管资源进行清理。
一般地,对于实现了IDisposeable接口的类型对象,在使用完毕后我会在finally块中调用Dispose方法来释放资源。如下:
FileManager fs = null; try { //fs... } finally { if (fs != null) { fs.Dispose(); } }
其实也可以以一种更简洁的方法来实现,那就是使用using语句,如下:
public void Test() { FileManager fs = null; using (fs = new FileManager()) { Console.WriteLine(fs.ToString()); } }
我们来看一下编译器生成的IL是什么样的?
.method public hidebysig instance void Test() cil managed { // 代码大小 45 (0x2d) .maxstack 2 .locals init ([0] class ConsoleApp.Example12.FileManager fs, [1] class ConsoleApp.Example12.FileManager CS$3$0000, [2] bool CS$4$0001) IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 IL_0003: newobj instance void ConsoleApp.Example12.FileManager::.ctor() IL_0008: dup IL_0009: stloc.0 IL_000a: stloc.1 .try { IL_000b: nop IL_000c: ldloc.0 IL_000d: callvirt instance string [mscorlib]System.Object::ToString() IL_0012: call void [mscorlib]System.Console::WriteLine(string) IL_0017: nop IL_0018: nop IL_0019: leave.s IL_002b } // end .try finally { IL_001b: ldloc.1 IL_001c: ldnull IL_001d: ceq IL_001f: stloc.2 IL_0020: ldloc.2 IL_0021: brtrue.s IL_002a IL_0023: ldloc.1 IL_0024: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0029: nop IL_002a: endfinally } // end handler IL_002b: nop IL_002c: ret } // end of method Code_12::Test
可以看到,编译器对于using语句是按照try…finally块模式来处理,并且在finally块中调用了Dispose方法,与上面我们手工编写的try…finally块代码实现了相同的功能。
在类似的环境中,using语句只能应用于实现了System.IDisposable接口的类型(或值类型)对象。