今天我們要來科普一下關於 C# 中垃圾回收機制的分代演算法,在上一篇曾提到當整個 Heap 記憶體空間在進行標記與整理時會造成系統處理效能上的負擔,而解決此問題的便是分代演算法。
使用分代演算法的優勢建立在一個前提狀態之上:
絕大多數物件新創建沒多久生命週期就會結束,有百分之九十的垃圾物件是上一個 GC 階段中產生的,而存活下來的物件在短期內被回收的機率很低,但每次標記動作仍會在這些長壽的物件上消耗時間與效能。
基於上述問題的解法是如何達成,分代演算法的解決方式是將回收目標按照生命長度分類,以不同的頻率各自進行回收,減少需要標記的數量。此方式已被證明為良好的解決方案而被廣泛地使用,包括 .NET Framework ramework 中也是採取此方案。
物件以不同的方式,例如創建的時間戳記,按照不同的存活時間進行分類。LOH(Large Object Heap)因效能考量而不會進行壓縮等等動作,在 .NET 中進行分代的區域為 SOH(Small Object Heap)對於這兩個區塊的詳細說明可參照前兩篇垃圾回收簡介。
一般常見的作法是將新創建的物件歸類為 Gen0,若此物件在此週期中未被 GC 會被稱為未回收物件,並在下一週期會將其提升至 Gen1;以此類推再下一週期則將其提升至 Gen2……等等。
越低階的分類越常觸發垃圾回收機制,越往高階提升則觸發次數遞減;雖然在 .NET Framework 中採用 3 代的系統,Gen0 及 Gen1 的空間較小,通常保持在 16MB 左右,成本較低;Gen2 的 GC 是所有物件的終點,大小有可能以 GB 為單位,在 .NET 中被稱為完全垃圾回收(Full Garbage Collection),花費時間也較長,在此分代中存活下來的物件不會進行位置上的調動,而是在原處等候下一次的 GC。值得注意的是每次 GC 循環之後, Gen0 中的物件數量必歸於零(被回收或是提升到 Gen1),Gen1 可能會保存 Gen0 提升上來的物件, Gen2 則除了自 Gen1 提升上來的物件以外,亦可儲存未被回收的物件。
當然分代演算法的概念可擴充到超越 3 代系統,依需求而定。
圖一、.NET 中的分代垃圾回收機制(圖片來源)
Gen0 | Gen1 | Gen2 |
存活時間最短、最新的物件,例如暫存變數。
GC觸發最頻繁的分代。 |
作為緩衝區,保留存活時間較短的物件。 | 放置存活時間最長的物件,例如處理程序中持續留存的靜態資料。 |
觸發 GC 機制的其中之一條件為"物件在進行管裡的記憶體空間上佔據超過一定閥值",此閥值可為一定範圍的多個數值,一開始各個分代的初始閥值最高範圍如下:
Gen 0: 256 K
Gen 1: 2 MBs
Gen 2: 10 MBs
此初始值會隨著程式的執行被 GC 所調整,增加數值的條件之一是該分代的物件存活率高,增加閥值使得 GC 的循環率降低(記憶體空間超越數值的頻率下降),減少 GC 頻繁地執行卻徒勞無功所耗費的效能。垃圾回收機制的目的是儘可能地蒐集垃圾物件,而非在明知大多數物件仍在使用中卻仍舊反覆地試圖回收。而在一般狀況底下,CLR 會試圖在使用的記憶體和 GC 的執行次數達到最大效能比的平衡。
程式碼背後的垃圾回收機制簡易介紹到此大致告一段落,提醒大家微軟的 Visual Studio 便有提供為 dump file 整理出記憶體樹狀圖的功能,此樹狀圖的逡巡方式與 GC Mark 階段的步驟一致,可以方便地觀察到留存在記憶體中未被回收的物件彼此間的參考關聯,若有興趣的讀者不妨可以玩看看。
Reference
https://docs.microsoft.com/zh-tw/dotnet/standard/garbage-collection/fundamentals
https://blogs.msdn.microsoft.com/abhinaba/2009/03/02/back-to-basics-generational-garbage-collection/
https://dotblogs.com.tw/jeffyang/2018/06/01/021755
https://www.telerik.com/blogs/understanding-net-garbage-collection