CLR回收非托管资源

一.非托管资源

      在《垃圾回收算法之引用计数算法》、《垃圾回收算法之引用跟踪算法》和《垃圾回收算法之引用跟踪算法》这3篇文章中,我们介绍了垃圾回收的一些基本概念和原理,但需要说明的是:这些文章中,介绍的都是对托管资源的回收,所谓托管资源,直白一点,你可以理解为托管堆上分配的对象,它由GC来自动管理。

      但本节,我们要介绍另外一种资源——非托管资源,它不是分配在托管堆上的资源,而是诸如文件、网络连接、网络套接字Socket、Windows互斥内核对象等其他的资源。

      C#程序有时也需要使用非托管资源,如我们使用FileStream打开文件句柄,并使用句柄操作文件,这里的文件就是非托管资源,当我们使用完FileStream时,GC会在某个时间点回收FileStream,但文件不是托管资源,GC对它一无所知,这样会造成内存的泄漏。为了应对这种情况,CLR提供了一种终结(Finalize)机制,以帮助程序释放非托管资源。

二.终结原理

 1.Finalize方法

   在语法上,C#Finalize方法非常类似于C++中的析构器,在类名前添加~符号来定义Finalize方法,CLRFinalize方法生成名为Finalizeprotected 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模式。

  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 > 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/46462047Philip Chan的回答

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

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

6.《.Net最佳实践》

posted @ 2017-03-05 12:52  gudi  阅读(471)  评论(0编辑  收藏  举报