CLR回收非托管资源
一.非托管资源
在《垃圾回收算法之引用计数算法》、《垃圾回收算法之引用跟踪算法》和《垃圾回收算法之引用跟踪算法》这3篇文章中,我们介绍了垃圾回收的一些基本概念和原理,但需要说明的是:这些文章中,介绍的都是对托管资源的回收,所谓托管资源,直白一点,你可以理解为托管堆上分配的对象,它由GC来自动管理。
但本节,我们要介绍另外一种资源——非托管资源,它不是分配在托管堆上的资源,而是诸如文件、网络连接、网络套接字Socket、Windows互斥内核对象等其他的资源。
C#程序有时也需要使用非托管资源,如我们使用FileStream打开文件句柄,并使用句柄操作文件,这里的文件就是非托管资源,当我们使用完FileStream时,GC会在某个时间点回收FileStream,但文件不是托管资源,GC对它一无所知,这样会造成内存的泄漏。为了应对这种情况,CLR提供了一种终结(Finalize)机制,以帮助程序释放非托管资源。
二.终结原理
1.Finalize方法
在语法上,C#的Finalize方法非常类似于C++中的析构器,在类名前添加~符号来定义Finalize方法,CLR为Finalize方法生成名为Finalize的protected overvide方法,方法体被try..finally方法块包裹,在finally中调用了base.Finalize方法.
如下所示:
public class TestClass { ~TestClass() { } }
生所IL代码如下:
Finalize机制允许CLR在判断对象为垃圾之后,但在回收垃圾之前执行一些代码,即执行Finalize方法,比如你可以在这些代码里回收非托管资源,下一步,CLR就可以回收托管堆上的资源(托管资源)了.
注意,并不是说你只能在Finalize方法中回收非托管资源,只是一种习惯性做法。
实际上,非托管资源也是一定要先于托管资源回收的。这是为什么呢?假设一个对象被判断为垃圾, 由于CLR对于非托管资源一无所知,CLR先回收了托管资源,如果在Finalize方法内部需要访问托管资源,则会造成内存泄漏,相反,非托管资源先释放掉,那么剩下的托管资源由于真正的不可达(既没有被非托管资源访问也没有被托管资源访问),就可以被GC垃圾回收了。
那一个对象的托管资源和非托管资源可不可以一起回收呢?答案也是不行的.因为,CLR采用一个特殊的、高优先级的专用线程调用Finalize方法(这样做是为了避免潜在的线程同步问题,使用应用程序的普通优先级线程就有可能发生空上问题),无法保证一起回收,它甚至不保证多个Finalize方法的调用顺序。
在接下来介绍的Finalize内部工作原理时,我们会介绍到freachable队列,特殊线程就是监控该队列的数据,freachable队列为空时,线程将睡眠,但一旦队列中有记录项出现时,线程就会被唤醒,将每一项从freachable队列中移除,同时调用每个对象的Finalize方法。
2.Fianlize的内部工作原理
我们来通过《CLR via C#》中的例子来说明Finalize的内部工作原理,在这之前我们要说明两个概念:
a.终结列表:用来存储实现了Finalize方法的对象指针列表,注意,CLR认为,如果你是从System.Object中继承了Finalize方法,则不会认为你是终结对象,但如果你重写了Finalize方法,则CLR认为对 象是终结对象,则会将它加入终结列表。那在程序运行的时候,对象何时加入终结列表呢?《CLR via C#》中说,在应用程序创建新对象时,该类型的实例构造函数被调用之前。
b.freachable:全称是Finalization Reachable List,它存储着所有被判断为垃圾的终结对象,等待着CLR专用线程对它的调用。
明白了以上的概念,现在我们来图解Finalize的内部工作原理。
如图所示,在初始状态下,在G0中,A C E F是可达的,C D I实现了Finalize方法,被加入了终结列表(即我们上面所说的概念a),freachable队列为空:
现在,GC开始扫描所有的根,形成对象可达图,注意GC会发现B D G H I J均为垃圾(即同步块索引中的一位标志为0),同时发现D和I虽然为垃圾,但是它是终结对象,因此将它们放入freachable列表中(上面所说的概念b),因为Finalize在被CLR专用线程调用时,这个对象必然要是存活的,所以使得freachable的这些终结对象(D和I)“复活”,同时它们引用的对象(J)也复活了。经过这两个步骤,GC形成了对象可达图,如下所示:
下面开始进行GC回收垃圾工作,在清除垃圾后,B G H被清掉,剩下的对象被压缩并提升至G1代中。随后,特殊的进程清空freachable队列,执行每个对象(这里是D和I)的Finalize方法:
执行完freachable的Finalize方法后,D I J现在没有任何对象引用它们,它们将在下次的GC组成对象可达图时,变得不可达(即垃圾)。在第二次(也可能是某一次)GC时,D I J被清除掉。
这里需要说明
a:终结对象的清除需要两次垃圾回收才能释放它们占用的垃圾;
b:这两次垃圾回收有可能不是连续的,因为GC执行第一次垃圾回收的,终结对象被提升至下一代,而在进行下一代的垃圾回收之前,前一代很有可能进行了1次或多次的垃圾回收。
3.Finalize方法的缺陷
a.因为可终结对象在调用时必须存活,造成可终结对象要经过两次释放才能真正释放掉资源,并在GC中提升至下一代,其引用的对象也会被提升,使对象活得比正常时间长,这增大了内存消耗;
b.Finalize方法的执行时间和执行顺序是控制不了的,因为只有GC完成后才会运行Finalize,而只有应用程序请求更多的内存而不够时才会进行GC;
c.根据b点说明,我们不可以在一个终结对象的Fialize方法中调用另一个终结对象,因为Finalize方法的执行顺序控制不了,我们无法保证调用时另一个终结对象还存在,但可以安全地访问值类型的类型;
d.CLR用专用线程调用Finalize方法来避免死锁,如果Finalize方法阻塞,则特殊线程也会发生阻塞,无法调用更多的Finalize方法,这使得GC永远回收不了终结对象占用的内存,内存则会一直泄漏;同时如果Finalize未处理的异常则会造成进程终止,无法捕捉该异常。
三.Dispose模式
前面我们介绍了Finalize的缺陷,其中c点,我们可以在程序中控制Finalize不要访问另外一个终结对象,对于d点,我们可以在Finalize中进行异常控制;对于a和b点,我们就不能控制了,这会带来一个致命的问题,如果我的非托管资源很少,在应对高并发的请求时,GC又不知道何时执行,非托管资源又在GC之后,对于非托管资源的释放成为性能的瓶颈,比如Socket等。
微软提供了一个Dispose模式来解决这个问题,它让我们能够显式地释放非托管资源,控制非托管资源的生存期。
实现了IDisposable接口,就实现了dispose模式。
- Dispose模式的设计原则 a.可以重复调用Dispose方法
- b.析构函数应该Dispose带参方法来释放非托管资源
- c.Dispose方法应该可以释放托管资源和非托管资源 d.Dispose方法应该调用GC.SuppressFinalize()方法,指示垃圾回收器不再重复回收该对象
-
e.CLR为继承了IDisposable接口的类提供了特殊的语法糖,使用using(MyDispose myOjb=new MyDispoe()){ … },它会在跳出using的区域时调用MyDispose的Dispose方法。
微软在官方网站上提供了Dispose模式的案例,如下所示
using System; class BaseClass : IDisposable { // 标志位:标志Dispose方法是否被调用过 bool disposed = false; // 实现IDisposable接口 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } // True时:释放托管和非托管资源,手工调用 //False时:只释放非托管资源,CLR专用线程调用 protected virtual void Dispose(bool disposing) { if (disposed) return; if (disposing) { // 释放托管资源 } // 释放所有的非托管资源 disposed = true; } ~BaseClass() { Dispose(false); } }
2.源码学习:看下.Net Framework中FileStrem的Dispose模式
a.FileStreamr的基类Stream实现了Dispose模式
public abstract class Stream : IDisposable { public void Dispose() { //通过Close方法释放托管资源和非托管资源,同时通知GC Close(); } public virtual void Close() { Dispose(true); GC.SuppressFinalize(this); } //虚方法,留给FileSteam去实现 protected virtual void Dispose(bool disposing) { } }
b.FileStream类,实现了Finalize方法,并重写了Dispose带参方法,当然FileStream的方法实现了很多功能,写法也较复杂,我们这里只需要了解一下关注的Dispose模式即可。
public class FileStream : Stream{ ~FileStream(){ if (_handle != null) { Dispose(false);//调用基类的Dispose方法,释放非托管资源 } } protected override void Dispose(bool disposing) { try { if (_handle != null && !_handle.IsClosed) { if (_writePos > 0) { FlushWrite(!disposing);//在这里释放资源 } } } finally { if (_handle != null && !_handle.IsClosed){ _handle.Dispose(); _canRead = false; _canWrite = false; _canSeek = false; base.Dispose(disposing); } } }
参考文档
1.《CLR via C#》(第4版)
2. https://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx
3. https://www.zhihu.com/question/46462047中Philip Chan的回答
4. http://blog.csdn.net/qing101/article/details/52484987
5. https://www.zhihu.com/question/29265003
6.《.Net最佳实践》