若是你常常看開源項目的源碼,你會發現不少Dispose方法中都有這麼一句代碼: GC.SuppressFinalize(this);
,看過一兩次可能無所謂,看多了就來了興趣,這篇就跟你們聊一聊。程序員
相信如今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); } }
先看一下官方對這個方法的解釋,以下所示:數據庫
// // 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
這個很好理解,你在C#中使用的值類型,引用類型都是統一受CLR分配和GC清理。api
在實際業務開發中,咱們的代碼不可能不與外界資源打交道,好比說文件系統,外部網站,數據庫等等,就拿寫入文件的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
不少時候程序員就是在使用完類以後由於種種緣由忘記了手動執行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> CLR在啓動時會構建一個「Finalize全局數組」和「待處理Finalize數組」 ,全部定義Finalize方法的類,它的引用地址所有額外再灌到「Finalize全局數組」中。
<2> CLR啓動一個專門的「Finalize線程」讓其全權監視「待處理Finalize數組」。
<3> GC在開啓清理前標記對象引用時,如發現某一個對象只有一個在Finalize數組中的引用,說明此對象是垃圾了,CLR將該對象地址轉移到另一個 「待處理Finalize」 數組中。
<4> 因爲該對象還存在引用,因此GC放了一馬,而後「Finalize線程」監視到了 「待處理Finalize數組」 新增的對象,取出該對象並執行該對象的Finalize方法。
<5> 因爲是破壞性取出,此時該對象再無任何引用,下次GC啓動時就會清理出去。
看文字有點繞,我畫一張圖幫你們理解下。
<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啓動就要被完全運走了。
若是你看懂了上面 Finalize
原理,再來看 SuppressFinalize
的解釋:‘請求 CLR 不要調用指定對象的終結器’。
就是說當你手動調用Dispose或者Close方法釋放了非託管資源後,經過此方法強制告訴CLR不要再觸發個人析構函數了,不然再執行析構函數至關於又作了一次清理非託管資源的操做,形成未知風險。
好了,本篇就說這麼多,但願你對有幫助。