.NET GC 精要(三)

本文講述了 .NET GC 的一些細節知識,內容大部分來自於書籍 Under the Hood of .NET Memory Management
(注:本文假設你瞭解 .NET 的基礎知識,譬如值類型,引用類型等)

進階

.NET 爲了處理非託管資源(unmanaged resource)的釋放問題,引入了終結器(Finalization)的機制,相關的代碼實現上也並不複雜,僅需要在類型中定義 析構函數 或者 Finalize 函數 即可(定義的 析構函數 或者 Finalize 函數會在對象"被清理"之後執行),示例代碼如下:

// method 1
class TestClass1
{
    ~TestClass1()
    {
        // release unmanaged resource here
    }
}

// method 2
class TestClass2
{
    void Finalize()
    {
        // release unmanaged resource here
    }
}

那麼 GC 是怎麼執行這些自定義的"終結"函數(析構函數 或者 Finalize 函數)的呢?你可能會認爲答案非常簡單:在對象被清理的時候調用執行即可.這種方式的確是可行的,但是會降低 GC 的性能:在自定義的"終結"函數中,代碼可能會執行數據庫關閉等耗時操作,如果我們在 GC 流程中直接同步調用這些"終結"函數,那麼整個 GC 流程就必須等待這些耗時操作的完成,性能上的損耗可想而知.

所以實際上, .NET 使用了與 GC 完全獨立的一個線程來處理終結器機制,這個"終結器"線程會定期的執行被清理對象的"終結"函數,細節實現上則主要涉及兩個隊列: Finalization Queue 和 fReachable Queue.

首先,如果一個對象定義了"終結"函數,那麼在創建該對象時,該對象的引用會被額外添加到 Finalization Queue 中,之後如果 GC 流程發現該對象已經沒有被引用,正常情況下該對象就會被清理,但是由於 Finalization Queue 中還存在(對該對象的)引用的關係,該對象不僅不會被清理,反而可能會進行"代提升"(譬如從 Gen 1 提升到 Gen 2,更多細節可以看之前的講述),另外的,該對象的引用會從 Finalization Queue 中清除,然後加入到 fReachable Queue 中,之後"終結器"線程便會執行 fReachable Queue 中引用對象的"終結"函數,然後清除 fReachable Queue 中的對象引用,之後該對象才能被 GC 流程真正清理.

還記的之前提過 GC roots(GC 根)嗎? Finalization Queue 和 fReachable Queue 其實也可以算作 GC roots 之一.

上述流程的示意圖:

在這裏插入圖片描述

在這裏插入圖片描述

(注意觀察圖中 Object Z(實現了"終結"函數) 的引用變化)

可以看到,終結器使對象的存在週期變長了,很多時候我們並不希望這種情況發生,緩解該問題的一個方法就是我們主動調用對象的"終結"函數,關於此有一個被稱爲 Dispose Pattern 的代碼模式,之前有寫過一些細節知識,有興趣的朋友可以看看(值得一提的是,之前文章中提到的 GC.SuppressFinalize 函數,原理上其實就是將對象引用從 Finalization Queue 中清除,這樣該對象就可以被 GC 正常清理了).

未完待續(to be continued)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章