如果你经常看开源项目的源码,你会发现很多Dispose方法中都有这么一句代码: GC.SuppressFinalize(this); ,看过一两次可能无所谓,看多了就来了兴趣,这篇就跟大家聊一聊。

一:背景

1. 在哪发现的

相信现在Mysql在.Net领域中铺的面越来越广了,C#对接MySql的MySql.Data类库的代码大家可以研究研究,几乎所有操作数据库的几大对象:MySqlConnection,MySqlCommand,MySqlDataReader以及内部的Driver都存在 GC.SuppressFinalize(this)代码。


public sealed class MySqlConnection : DbConnection, ICloneable
{
   public new void Dispose()
   
{
       Dispose(disposing: true);
       GC.SuppressFinalize(this);
   }
}

public sealed class MySqlCommand : DbCommand, IDisposable, ICloneable
{
   public new void Dispose()
   
{
       Dispose(disposing: true);
       GC.SuppressFinalize(this);
   }
}

2. GC.SuppressFinalize 场景在哪里

先看一下官方对这个方法的解释,如下所示:

        //
       // Summary:
       //     Requests that the common language runtime not call the finalizer for the specified
       //     object.
       //
       // Parameters:
       //   obj:
       //     The object whose finalizer must not be executed.
       //
       // Exceptions:
       //   T:System.ArgumentNullException:
       //     obj is null.
       [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
       [SecuritySafeCritical]
       public static void SuppressFinalize(object obj);

意思就是说: 请求 CLR 不要调用指定对象的终结器,如果你对终结器的前置基础知识不足,那这句话肯定不是很明白,既然都执行了Dispose,说明非托管资源都被释放了,怎么还压制CLR不要调用Finalize呢?删掉和不删掉这句代码有没有什么严重的后果,GC类的方法谁也不敢动哈。。。为了彻底讲清楚,有必要说一下Finalize整个原理。

二:资源管理

我们都知道C#是一门托管语言,它的好处就是不需要程序员去关心内存的分配和释放,由CLR统一管理,这样编程门槛大大降低,天下攘攘皆为利来,速成系的程序员就越来越多~

1. 对托管资源和非托管资源理解

<1> 托管资源

这个很好理解,你在C#中使用的值类型,引用类型都是统一受CLR分配和GC清理。

<2> 非托管资源

在实际业务开发中,我们的代码不可能不与外界资源打交道,比如说文件系统,外部网站,数据库等等,就拿写入文件的StreamWriter举例,如下代码:

        public static void Main(string[] args)
       
{
           StreamWriter sw = new StreamWriter("xxx.txt");
           sw.WriteLine("....");
       }

为什么能够写入文件?那是因为我们的代码是请求windows底层的Win32 Api帮忙写入的,这就有意思了,因为这个场景有第三者介入,sw是引用类型受CLR管理,win32 api属于外部资源和.Net一点关系都没有,如果你在用完sw之后没有调用close方法的话,当某个时候GC回收了托管堆上的sw后,这给被打开的win32 api文件句柄再也没有人可以释放了,资源就泄露了,如果没看懂,我画张图:

三:头疼的非托管资源解决方案

1. 使用析构函数

很多时候程序员就是在使用完类之后因为种种原因忘记了手动执行Close方法造成了资源泄露,那有没有一种机制可以在GC回收堆对象的时候回调我的一个自定义方法呢?如果能实现这样我就可以在自定义方法中做全局的控制。

其实这个自定义方法就是析构函数,接下来我把上面的 StreamWriter 改造下,将 Close() 方法放置在析构函数中,先看一下代码:


   public class Program
   {

       public static void Main(string[] args)
       
{
           MyStreamWriter sw = new MyStreamWriter("xxx.txt");
           sw.WriteLine("....");

           GC.Collect();
           Console.ReadLine();
       }
   }

   public class MyStreamWriter : StreamWriter
   {
       public MyStreamWriter(string filename) : base(filename) { }

       ~MyStreamWriter()
       {
           Console.WriteLine("嘿嘿,忘记调用Close方法了吧!我来帮你");
           base.Dispose(false);
           Console.WriteLine("非托管资源已经帮你释放啦,不要操心了哈");
       }
   }

--------- output -----------

嘿嘿,忘记调用Close方法了吧!我来帮你
非托管资源已经帮你释放啦,不要操心了哈

四:析构函数被执行的底层分析

让GC来通知我的回调方法这本身仔细想想,在垃圾回收时,CLR不是将所有线程都挂起了吗?怎么还有活动的线程,而且这个线程是来自哪里?线程池吗?好,先从理论跟和大家分析一下,析构函数在CLR层面称为Finalize方法,为了方便后面通过windbg去验证,这里统一都叫Finalize方法,提前告知。

1. 原理步骤

<1> CLR在启动时会构建一个“Finalize全局数组”和“待处理Finalize数组” ,所有定义Finalize方法的类,它的引用地址全部额外再灌到“Finalize全局数组”中。

<2> CLR启动一个专门的“Finalize线程”让其全权监视“待处理Finalize数组”。

<3> GC在开启清理前标记对象引用时,如发现某一个对象只有一个在Finalize数组中的引用,说明此对象是垃圾了,CLR将该对象地址转移到另外一个 “待处理Finalize” 数组中。

<4> 由于该对象还存在引用,所以GC放了一马,然后“Finalize线程”监视到了 “待处理Finalize数组” 新增的对象,取出该对象并执行该对象的Finalize方法。

<5> 由于是破坏性取出,此时该对象再无任何引用,下次GC启动时就会清理出去。

看文字有点绕,我画一张图帮大家理解下。

2. windbg验证

<1> 修改Main代码如下,抓一下dump文件看看 MyStreamWriter是否在Finalize全局数组中。


       public static void Main(string[] args)
       
{
           MyStreamWriter sw = new MyStreamWriter("xxx.txt");
           sw.WriteLine("....");

           Console.ReadLine();
       }

``` C#

0:000> !FinalizeQueue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 13 finalizable objects (0000018c2a9b7a80->0000018c2a9b7ae8)generation 1 has 0 finalizable objects (0000018c2a9b7a80->0000018c2a9b7a80)generation 2 has 0 finalizable objects (0000018c2a9b7a80->0000018c2a9b7a80)Ready for finalization 0 objects (0000018c2a9b7ae8->0000018c2a9b7ae8)Statistics for all finalizable objects (including all objects ready for finalization):
             
MT    Count    TotalSize Class Name00007ff8e7afb2a8        1           32 System.Runtime.InteropServices.NativeBuffer+EmptySafeHandle00007ff8e7a94078        1           32 Microsoft.Win32.SafeHandles.SafePEFileHandle00007ff8e7a843b0        1           32 Microsoft.Win32.SafeHandles.SafeFileMappingHandle00007ff8e7a84320        1           32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle00007ff8e7b001b8        1           40 System.Runtime.InteropServices.SafeHeapHandleCache00007ff8e7ad6df0        1           40 System.Runtime.InteropServices.SafeHeapHandle00007ff8e7b133d0        2           64 Microsoft.Win32.SafeHandles.SafeRegistryHandle00007ff8e7a995d0        2           64 Microsoft.Win32.SafeHandles.SafeFileHandle00007ff8e7a93b48        1           64 System.Threading.ReaderWriterLock00007ff8e7b14d38        1          104 System.IO.FileStream00007ff889d45b18        1          112 ConsoleApp2.MyStreamWriterTotal 13 objects

很惊喜的看到 MyStreamWriter 就在其中,符合图中所示。

<2> 查看是否有专门的 “Finalize线程” ,可以通过 !threads 命令查看。


0:000> !threads
ThreadCount:      2
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                                                       Lock  
      ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
  0    1  bf4 0000018c2a990f00    2a020 Preemptive  0000018C2C429168:0000018C2C429FD0 0000018c2a965220 1     MTA
  6    2 44f4 0000018c2a9b9450    2b220 Preemptive  0000000000000000:0000000000000000 0000018c2a965220 0     MTA (Finalizer)

看到没,线程2标记了 MTA (Finalizer), 说明果然有执行Finalizer方法的专有线程。

<3> 由于水平有限,不知道怎么去看 “待处理Finalize数组”,所以只能验证等GC回收之后,看下 “Finalize全局数组”中是否还存在MyStreamWriter即可。


       public static void Main(string[] args)
       
{
           MyStreamWriter sw = new MyStreamWriter("xxx.txt");
           sw.WriteLine("....");
           GC.Collect();
           Console.ReadLine();
       }

------- output ---------

嘿嘿,忘记调用Close方法了吧! 我来帮你
非托管资源已经帮你释放啦,不要操心了哈


0:000> !FinalizeQueue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 5 finalizable objects (0000021e8051a798->0000021e8051a7c0)generation 1 has 5 finalizable objects (0000021e8051a770->0000021e8051a798)generation 2 has 0 finalizable objects (0000021e8051a770->0000021e8051a770)Ready for finalization 0 objects (0000021e8051a7c0->0000021e8051a7c0)Statistics for all finalizable objects (including all objects ready for finalization):
             
MT    Count    TotalSize Class Name00007ff8e7afb2a8        1           32 System.Runtime.InteropServices.NativeBuffer+EmptySafeHandle00007ff8e7a94078        1           32 Microsoft.Win32.SafeHandles.SafePEFileHandle00007ff8e7a843b0        1           32 Microsoft.Win32.SafeHandles.SafeFileMappingHandle00007ff8e7a84320        1           32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle00007ff8e7b001b8        1           40 System.Runtime.InteropServices.SafeHeapHandleCache00007ff8e7ad6df0        1           40 System.Runtime.InteropServices.SafeHeapHandle00007ff8e7a995d0        2           64 Microsoft.Win32.SafeHandles.SafeFileHandle00007ff8e7a93b48        1           64 System.Threading.ReaderWriterLock00007ff8e7a96a10        1           96 System.Threading.ThreadTotal 10 objects

可以看到这时候 “全局数组” 没有引用了,再看一下托管堆是否还存在 MyStreamWriter以及线程栈中是否还有对象引用地址。


0:000> !dumpheap
        Address               MT     Size
00007ff889d25b00        1          112 ConsoleApp2.MyStreamWriter

Total 423 objects

0:000> !clrstack -l
OS Thread Id: 0x1b00 (0)
       Child SP               IP Call Site
0000007ecdffe9e0 00007ff8e88c20cc System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
   LOCALS:
       <no data>
       <no data>
       <no data>
       <no data>
       <no data>
       <no data>
0000007ecdffea70 00007ff8e88c1fd5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
   LOCALS:
       <no data>
       <no data>
0000007ecdffead0 00007ff8e80770f4 System.IO.StreamReader.ReadBuffer()
   LOCALS:
       <no data>
       <no data>
0000007ecdffeb20 00007ff8e8077593 System.IO.StreamReader.ReadLine()
   LOCALS:
       <no data>
       <no data>
       <no data>
       <no data>
0000007ecdffeb80 00007ff8e8a68b0d System.IO.TextReader+SyncTextReader.ReadLine()
0000007ecdffebe0 00007ff8e8860d98 System.Console.ReadLine()
0000007ecdffec10 00007ff889e30959 ConsoleApp2.Program.Main(System.String[])
0000007ecdffeea8 00007ff8e9396c93 [GCFrame: 0000007ecdffeea8]

可以看到MyStreamWriter还是存在于托管堆,但是线程栈已再无它的引用地址,就这样告别了全世界,下次GC启动就要被彻底运走了。

五:回头再看 SuppressFinalize

如果你看懂了上面 Finalize 原理,再来看 SuppressFinalize的解释:‘请求 CLR 不要调用指定对象的终结器’。

就是说当你手动调用Dispose或者Close方法释放了非托管资源后,通过此方法强制告诉CLR不要再触发我的析构函数了,否则再执行析构函数相当于又做了一次清理非托管资源的操作,造成未知风险。


©著作权归作者所有:来自51CTO博客作者mb5fd86a704dffe的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. C#中方法的调用
  2. C#9.0 终于来了,您还学的动吗? 带上VS一起解读吧!
  3. 第7章 0202-面向对象编程基础,学习心得、笔记(类声明,类的实例化,类
  4. Linq 下的扩展方法太少了,您期待的 MoreLinq 来啦
  5. Enumerable 下又有新的扩展方法啦,快来一睹为快吧
  6. await,async 我要把它翻个底朝天,这回你总该明白了吧
  7. PHP类成员重载与命名空间
  8. 多线程基础知识
  9. Android Studio生成签名文件方法

随机推荐

  1. golang map需要make吗
  2. golang怎么生成不重复随机数
  3. golang slice如何拷贝
  4. golang map无法删除元素吗
  5. golang如何传不定参数
  6. golang slice检查元素是否存在
  7. golang map是否有顺序
  8. golang不用指针可以吗
  9. golang sleep为什么没有返回值
  10. golang map判断key是否存在