栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

从GC的SuppressFinalize方法带你深刻认识Finalize底层运行机制

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

从GC的SuppressFinalize方法带你深刻认识Finalize底层运行机制

如果你经常看开源项目的源码,你会发现很多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 Name
00007ff8e7afb2a8 1    32 System.Runtime.InteropServices.NativeBuffer+EmptySafeHandle
00007ff8e7a94078 1    32 Microsoft.Win32.SafeHandles.SafePEFileHandle
00007ff8e7a843b0 1    32 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
00007ff8e7a84320 1    32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
00007ff8e7b001b8 1    40 System.Runtime.InteropServices.SafeHeapHandleCache
00007ff8e7ad6df0 1    40 System.Runtime.InteropServices.SafeHeapHandle
00007ff8e7b133d0 2    64 Microsoft.Win32.SafeHandles.SafeRegistryHandle
00007ff8e7a995d0 2    64 Microsoft.Win32.SafeHandles.SafeFileHandle
00007ff8e7a93b48 1    64 System.Threading.ReaderWriterLock
00007ff8e7b14d38 1   104 System.IO.FileStream
00007ff889d45b18 1   112 ConsoleApp2.MyStreamWriter
Total 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 Name
00007ff8e7afb2a8 1    32 System.Runtime.InteropServices.NativeBuffer+EmptySafeHandle
00007ff8e7a94078 1    32 Microsoft.Win32.SafeHandles.SafePEFileHandle
00007ff8e7a843b0 1    32 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
00007ff8e7a84320 1    32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
00007ff8e7b001b8 1    40 System.Runtime.InteropServices.SafeHeapHandleCache
00007ff8e7ad6df0 1    40 System.Runtime.InteropServices.SafeHeapHandle
00007ff8e7a995d0 2    64 Microsoft.Win32.SafeHandles.SafeFileHandle
00007ff8e7a93b48 1    64 System.Threading.ReaderWriterLock
00007ff8e7a96a10 1    96 System.Threading.Thread
Total 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:
 
 
 
 
 
 
0000007ecdffea70 00007ff8e88c1fd5 System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
    LOCALS:
 
 
0000007ecdffead0 00007ff8e80770f4 System.IO.StreamReader.ReadBuffer()
    LOCALS:
 
 
0000007ecdffeb20 00007ff8e8077593 System.IO.StreamReader.ReadLine()
    LOCALS:
 
 
 
 
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不要再触发我的析构函数了,否则再执行析构函数相当于又做了一次清理非托管资源的操作,造成未知风险。

好了,本篇就说这么多,希望你对有帮助。

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号