.NET CLR 的垃圾收集器 (GC) 將物件依大小劃分為小型物件(< 85,000 位元組)與大型物件(≥ 85,000 位元組),後者配置於專屬的大型物件堆 (Large Object Heap, LOH)。LOH 的特殊回收行為——不壓縮、僅第 2 代回收、自由空間鏈表管理——對大型陣列、巨型字串、序列化緩衝區等常見場景有深遠的效能影響。本文深入剖析 LOH 分配與回收機制、碎片化成因、監控指標與調優策略。
CLR 托管堆在邏輯上分為三代:Gen 0、Gen 1、Gen 2。小型物件出生於 Gen 0,存活者晉升至 Gen 1、Gen 2。大型物件則直接配置於 LOH,**邏輯上隸屬 Gen 2**,僅在第 2 代回收 (Full GC) 時被掃描回收。
物理上,GC 透過 VirtualAlloc 向作業系統保留記憶體區段:
啟動時各配置一個初始區段,隨著配置需求以較小塊提交記憶體。
壓縮意味著將物件在記憶體中搬移,需更新所有參考該物件的指標。對大型物件(動輒數 MB 甚至數百 MB),搬移成本極高(記憶體複製 + 指標修正),且大型物件通常壽命較長、參考較少,壓縮收益不彰。因此 CLR 選擇:
警告:雖目前不壓縮,但未來版本可能改變。若需確保物件不被移動,必須使用 GCHandle.Alloc(obj, GCHandleType.Pinned) 固定物件。
配置大型物件時,若 LOH 可用空間不足:
VirtualAlloc)OutOfMemoryException第 2 代 GC 觸發條件:
GC.Collect() 或 GC.Collect(GC.MaxGeneration)關鍵特性:LOH 回收與 Gen 2 綁定。若應用程式頻繁配置臨時大型物件,會強制頻繁 Full GC,同時掃描龐大 Gen 2,造成嚴重效能抖動。

圖 1:LOH 回收後形成自由空間

圖 2:GC 釋放空區段給 OS
!dumpheap -type Free 觀察。VirtualAlloc/VirtualFree 導致進程位址空間出現大量細小 MEM_FREE 區域,雖托管堆內部完好,但無法配置大區段。需用 !address (WinDbg) 分析。臨時大型物件是 VM 碎片主因:頻繁申請/釋放區段 → 位址空間破碎 → 後續大型配置失敗 → OOM。

圖 3:效能監視器新增 LOH 計數器
關鍵計數器:
SOS 偵錯指令 (.loadby sos mscorwks):
!eeheap -gc // 顯示各堆區段範圍與大小 !dumpheap -stat // 統計各類型物件計數與大小 !dumpheap -stat 023e1000 033db630 // 限定 LOH 範圍統計
範例輸出顯示 LOH 佔用 49.7 MB,主要為 System.Object[] (8 MB) 與 System.Byte[] (6.6 MB),自由空間 2 MB。
頻繁配置/釋放大型陣列(如網路緩衝區、序列化暫存區)應改用物件池。Yun Jin 部落格提供 BufferPool 實作參考。核心模式:
// 取用
byte[] buffer = BufferPool.Rent(size);
try { ... } finally { BufferPool.Return(buffer); }
若大型陣列元素為參考型別(如 Node[]),GC 掃描每個元素成本高。改用索引而非直接參考:
// 高 GC 成本
class Node { Data d; Node left, right; }
Node[] tree = new Node[1000000];
// 低 GC 成本
class Node { Data d; uint leftIndex, rightIndex; }
Node[] tree = new Node[1000000]; // 元素無參考,GC 忽略陣列內部
透過宿主 API 指定 STARTUP_HOARD_GC_VM 啟動旗標,GC 釋放區段時僅將記憶體退回備用清單 (VirtualFree(MEM_DECOMMIT)),不歸還 OS (MEM_RELEASE),下次需求直接從備用清單滿足,避免 VM 碎片。適用於長期運行、記憶體使用穩定的伺服器應用。
固定物件阻礙 GC 壓縮 SOH,若大量固定小物件反而惡化碎片。僅在 P/Invoke 需傳遞緩衝區指標時短暫固定。
# Gen 2 Collections 與 Large Object Heap Size,SOS 分析物件分佈GCHandle.Alloc掌握 LOH 行為,是構建高效能、低延遲 .NET 系統的關鍵能力之一。