CLR回收非托管资源

时间:2023-03-09 16:03:02
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代码如下:

CLR回收非托管资源

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队列为空:

    CLR回收非托管资源

    现在,GC开始扫描所有的根,形成对象可达图,注意GC会发现B D G H I J均为垃圾(即同步块索引中的一位标志为0),同时发现D和I虽然为垃圾,但是它是终结对象,因此将它们放入freachable列表中(上面所说的概念b),因为Finalize在被CLR专用线程调用时,这个对象必然要是存活的,所以使得freachable的这些终结对象(D和I)“复活”,同时它们引用的对象(J)也复活了。经过这两个步骤,GC形成了对象可达图,如下所示:

    CLR回收非托管资源

    下面开始进行GC回收垃圾工作,在清除垃圾后,B G H被清掉,剩下的对象被压缩并提升至G1代中。随后,特殊的进程清空freachable队列,执行每个对象(这里是D和I)的Finalize方法:

    CLR回收非托管资源

   执行完freachable的Finalize方法后,D I J现在没有任何对象引用它们,它们将在下次的GC组成对象可达图时,变得不可达(即垃圾)。在第二次(也可能是某一次)GC时,D I J被清除掉。

CLR回收非托管资源

这里需要说明

a:终结对象的清除需要两次垃圾回收才能释放它们占用的垃圾;

b:这两次垃圾回收有可能不是连续的,因为GC执行第一次垃圾回收的,终结对象被提升至下一代,而在进行下一代的垃圾回收之前,前一代很有可能进行了1次或多次的垃圾回收。

CLR回收非托管资源

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模式。

  1. Dispose模式的设计原则
  2. a.可以重复调用Dispose方法

  1. b.析构函数应该Dispose带参方法来释放非托管资源
  2. c.Dispose方法应该可以释放托管资源和非托管资源
  3. 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 > ) {
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/46462047Philip Chan的回答

4. http://blog.csdn.net/qing101/article/details/52484987

5. https://www.zhihu.com/question/29265003

6.《.Net最佳实践》