4

.NET 記憶體管理探索(1) - Managed Heap、SOH、LOH 與 GC

 3 years ago
source link: https://blog.darkthread.net/blog/managed-heap-study/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Managed Heap、SOH、LOH 與 GC-黑暗執行緒

前幾天解決完非典型 Out Of Memory 茶包,感覺自己雖然寫了這麼多年 .NET,對記憶體管理的了解仍偏虛浮,只知道背後有個強大的 GC 會負責找記憶體空間放物件,物件不用了會自動回收空間,完全不用我們操煩。需要物件時 new 一下,不要殘留變數、屬性指向超出變數範圍(Scope)的物件,.NET Runtime 自會打理好大小事。但面對記憶體洩漏、記憶體不足等疑難雜症,缺少運作原理知識,就會不知從何下手。

這系列文章算是老鳥蹲馬步,回頭把 .NET 記憶體管理的一些術語觀念弄清楚,有了這個基礎,未來處理 .NET 記憶體茶包能更容易抽絲剝繭找出真相。第一篇先從幾個關鍵術語談起:Managed Heap、Small Object Heap(SOH)、Large Object Heap(LOH)、Garbage Collector(GC)。

Reference Type 類 .NET 物件 (延伸閱讀:Value Type vs Reference Type 自我測驗),會被保存在名為 Managed Heap 的記憶體區塊。建立新物件時,會從 Managed Heap 找出一塊空間存放該物件。Managed Heap 分為 Small Object Heap (SOH) 及 Large Object Heap (LOH),以 85,000 Bytes 為門檻(Large Object Heap Threadshold),大於 85,000 Bytes 的物件放在 LOH,小於這個大小的則一律放在 SOH。(註:Large Object Heap Threadshold 可以調整)

.NET Runtime 在啟動程序時,GC 會配置兩個記憶體區段作為 SOH 跟 LOH。SOH 又會依物件存活時間長短區分 Generation (層代、世代),由年輕到長壽分為 G0、G1、G2,由 Garbage Collector (GC) 負責管理。

區分 Generation 有不少好處:回收整理(Collect)記憶體以 Generation 為單位,會比每次處理一整個 Heap 有效率,並且可依據各 Generation 安排適合的回收頻率,像是 G0 多為暫時性或區域變數,建立與丟棄的頻率都高,需要較常執行回收;G2 異動最少,久久整理一次即可。回收很耗費 CPU 資源,過度頻率執行將影響程式效能,GC 會自行判斷時機,一般不需由程式觸發,只有極少的狀況需要程式強制執行 GC.Collect()。另外,GC 會自動調配 G0、G1、G2 的小大,確保運作效率。

小型新物件建立時一律先放在 G0,當 G0 空間不足,GC 會進行清理,回收失效物件佔用的記憶體,並進行壓實的動作(Compact,搬移物件讓物件記憶體位址相接,物件間不要出現空隙)。回收未使用物件整理完 G0,留下還在使用中的物件會搬到 G1,G0 回收時不需要重新檢查 G1、G2;當 G0 回收完,G0 空間仍不足,GC 會對 G1 執行回收;G1 回收時,仍存留物件將升級到 G2;若 G1 也不夠用,則會對 G2 進行回收。
(以下圖片來自 Memory Management in C# by Adam Thorn,展示每次記憶體回收後,G0、G1、G2 的物件存放變化)

圖片來源

大型物件新增時不放在 SOH,而是放在 LOH。LOH 不區分 Generation,視同 G2,但有時也被稱為 G3,它會在回收 G2 時一起執行回收,但有一點重要差異 - LOH 回收時不會 Compact (壓實) 搬移物件(物件資料可能高達數百 KB 到數百 MB,搬移成本過高),回收物件空出來的記憶體會形成空洞,這不連續的可用空間稱為 LOH 破碎(LOH Fragmentation),嚴重時可能出現可用空間總和夠大,但找不出一整段連續空間放置新物件的窘況,導致 Out of Memory Exception。

圖片來源

GC 會依回收後留存物件比率調整各 Generation 記憶體大小,避免因大久沒回收導致無用資料長期佔用實體記憶體(Working Set),又不會因頻繁回收影響效能,力求在二者間取得平衡。

G0 與 G1 又稱為 Ephemeral Generation,必須配置在名為 Ephemeral Segment 的記憶體區間,Ephemeral Segment 也可能包含 G2 物件;Ephemeral Segment 大小依作業系統有所不同:(Server GC Mode 每個 Logical CPU 會有一個 GC Thread,故 Ephemeral Segment 反而較小)

Workstation/Server GC32-bit64-bitWorkstation GC16 MB256 MBServer GC64 MB4 GBServer GC with > 4 logical CPUs32 MB2 GBServer GC with > 8 logical CPUs16 MB1 GB

G0 與 G1 只能放在固定大小的 Ephemeral Segment,在 Ephemeral Segment 之外,還可以有 0 到多個 G2 Segment,其中只會包含 G2 物件,加上 LOH 並不會佔用 Ephemeral Segment,故記憶體使用空間不會因此受限。

看完理論,來實際演練。以下程式用來驗證幾件事:

  1. 以 85000 Bytes 區分大小物件,決定建在 SOH(G0) 還是 LOH(G2)
  2. G0 回收存活物件會升級成 G1,G1 回收物件升級到 G2

這個實驗依靠 GC.GetGeneration(objectVar) 偵測物件位於 G0、G1 或 G2,GC.Colect(generationNo) 指定進行 G0/G1/G2 回收。測試程式碼如下:

static void Main(string[] args)
{
    try
    {
        Test_Generation();
    }
    catch (Exception ex)
    {
        Console.WriteLine("ERROR-" + ex.Message);
    }
    Console.ReadLine();
}

static void Test_Generation()
{
    var x85minus = new byte[84999 - 12];
    var x85 = new byte[85000 - 12];
    var x85plus = new byte[85001 - 12];
    Console.WriteLine($"Object < 85000bytes => G{GC.GetGeneration(x85minus)}");
    Console.WriteLine($"Object = 85000bytes => G{GC.GetGeneration(x85)}");
    Console.WriteLine($"Object > 85000bytes => G{GC.GetGeneration(x85plus)}");

    var x = new byte[4];
    Console.WriteLine($"Before G0 colection => G{GC.GetGeneration(x)}");
    GC.Collect(0);
    Console.WriteLine($"After 1st G0 collection => G{GC.GetGeneration(x)}");
    GC.Collect(0);
    Console.WriteLine($"After 2nd G0 collection => G{GC.GetGeneration(x)}");
    GC.Collect(1);
    Console.WriteLine($"After G1 collection => G{GC.GetGeneration(x)}");
    GC.Collect(2);
    Console.WriteLine($"After G2 collection => G{GC.GetGeneration(x)}");
}

如下圖所示,我建了三個 byte[],大小分別為 84999、85000、85001 (註:每個 byte[] 物件除了陣列長度需外加 12 Bytes 才是實際大小,故長度減 12 以精準控制物件大小),84999 在 G0(SOH)、85000 及 85001 在 G2(LOH)。第二實驗,x 物件一開始建在 G0,G0 回收後升到 G1,第二次 G0 回收仍留在 G1,執行 G1 回收後升到 G2,符合預期。

小結,本文概略介紹了 Managed Heap、GC、SOH、LOH 等處理 .NET 記憶體問題可能用到的術語,共透過實驗觀察,物件大小與保存位置的差異,以及 G0/G1/G2 回收及 SOH 物件世代升級的行為。

【參考資料】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK