從GC的SuppressFinalize方法帶你深入認識Finalize底層運行機制

若是你常常看開源項目的源碼,你會發現不少Dispose方法中都有這麼一句代碼: GC.SuppressFinalize(this); ,看過一兩次可能無所謂,看多了就來了興趣,這篇就跟你們聊一聊。程序員

一:背景

1. 在哪發現的

相信如今Mysql在.Net領域中鋪的面愈來愈廣了,C#對接MySql的MySql.Data類庫的代碼你們能夠研究研究,幾乎全部操做數據庫的幾大對象:MySqlConnection,MySqlCommand,MySqlDataReader以及內部的Driver都存在 GC.SuppressFinalize(this)代碼。sql

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統一管理,這樣編程門檻大大下降,天下攘攘皆爲利來,速成系的程序員就愈來愈多~windows

1. 對託管資源和非託管資源理解

<1> 託管資源

這個很好理解,你在C#中使用的值類型,引用類型都是統一受CLR分配和GC清理。api

<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文件句柄再也沒有人能夠釋放了,資源就泄露了,若是沒看懂,我畫張圖:app

三:頭疼的非託管資源解決方案

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:
        <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不要再觸發個人析構函數了,不然再執行析構函數至關於又作了一次清理非託管資源的操做,形成未知風險。

好了,本篇就說這麼多,但願你對有幫助。


如您有更多問題與我互動,掃描下方進來吧~


相關文章
相關標籤/搜索