如果你經常看開源專案的原始碼,你會發現很多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:
<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不要再觸發我的解構式了,否則再執行解構式相當于又做了一次清理非托管資源的操作,造成未知風險,
好了,本篇就說這么多,希望你對有幫助,
如您有更多問題與我互動,掃描下方進來吧~


轉載請註明出處,本文鏈接:https://www.uj5u.com/net/44851.html
標籤:C#
