首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

.NET 陷阱:大型物件堆 (LOH) 導致的記憶體碎片與對策

     編輯:關於.NET

現象:明明還有大量可用記憶體,卻拋出 OutOfMemoryException

開發過程中常遇到詭異情況:應用程式載入大量資料後,偶爾拋出 OutOfMemoryException,但記憶體分析工具顯示仍有大量可用空間。直覺告訴我們:總記憶體夠了,但找不到「連續」的大塊記憶體——記憶體碎片化了。不過,.NET GC 不是會壓縮記憶體嗎?為何碎片依然存在?

罪魁禍首正是大型物件堆 (Large Object Heap, LOH)。本文深入剖析 LOH 的運作機制,並提供實戰解決策略。

驗證實驗:驚人的 1% 記憶體使用率

參考經典範例,交替配置小陣列 (90,000 bytes) 與遞增大陣列 (從 16MB 開始,每次 +1 byte)。在 32 位元程序 (2GB 虛擬位址空間) 中,拋出 OOM 時,有效記憶體僅佔約 23 MB (約 1%)。

// 核心迴圈
var smallBlocks = new List<byte[]>();
while (true) {
    GC.Collect();
    bigBlock = new byte[largeBlockSize++]; // 大物件進入 LOH
    smallBlocks.Add(new byte[smallBlockSize]); // 小物件進入 Gen 0
    count++;
}

結果揭示:LOH 不壓縮、配置策略優先尾部、碎片無法重用,導致虛擬位址空間被大量「空洞」浪費。

根因深度解析:LOH 的特殊規則

1. 門檻與代系

  • 大小 ≥ 85,000 bytes 的物件 → 直接配置於 LOH。
  • LOH 物件出生即屬於 Generation 2,僅在 Gen 2 GC 時回收。

2. 不壓縮

Gen 0/1 GC 會壓縮記憶體(移動存活物件、合併空閒空間),但 LOH 從不壓縮。回收後留下的空洞永久存在,除非被相同或更大尺寸的新物件重用。

3. 配置策略:優先尾部,再向 OS 申請,最後才掃描空洞

LOH 配置策略示意圖

  1. 嘗試在 LOH 尾部配置(指標遞增,極快)。
  2. 尾部空間不足 → 向作業系統申請新記憶體區段(擴大虛擬位址空間)。
  3. 申請失敗 (OOM) → 才會掃描中間的空洞嘗試重用。

實驗中,大陣列不斷增大,尾部空間永遠不夠,持續向 OS 申請新區段,而前方留下的 16MB、16.1MB... 空洞因太小而無法重用,最終耗盡 2GB 位址空間。

LOH 碎片化最終狀態

解決策略

策略一:拆分大物件,避開 LOH (推薦)

將大陣列/集合拆解為多個 < 85,000 bytes 的小區塊。例如實作一個 BlockList<T> (類似 C++ deque),內部以陣列陣列 T[][] 儲存,每塊大小上限設為 80,000 bytes。對外仍實作 IList<T>,呼叫端無感知。

public class BlockList<T> : IList<T> {
    private T[][] blocks;
    private int[] blockSizes;
    private const int MaxBlockSize = 80000 / sizeof(T); // 確保 < 85KB
    // ... 實作 Add, Insert, RemoveAt, 索引器等,邏輯為跨區塊定位
    private bool ExpandBlock(int blockId) { /* 擴充區塊至 MaxBlockSize */ }
    private void UseNewBlock() { /* 新增區塊 */ }
}

實戰案例:系統以 List<float> 儲存大量曲線資料,導致 LOH 壓力過大。改用 BlockList<float> 後問題徹底解決。

策略二:重用大物件 (Object Pooling)

對於不可拆分的大緩衝區 (如影像處理、網路封包),建立物件池,ArrayPool<T>.Shared (.NET Core/.NET 5+) 或自行實作 ConcurrentBag<byte[]>,避免頻繁配置/回收造成碎片。

策略三:定期重啟進程 (權宜之計)

釋放虛擬位址空間,消除碎片。適用於 Worker Role、Windows Service 等可控環境。

64 位元環境的誤區

雖然 64 位元擁有龐大位址空間 (TB 級),不易發生 OOM,但 LOH 碎片會導致記憶體分頁交換頻率飆升 (Working Set 膨脹),嚴重拖垮效能。因此 64 位元同樣需重視 LOH 管理。

總結

LOH 不壓縮、優先尾部配置的特性,使其成為大物件頻繁配置/釋放場景下的效能殺手。核心對策是「拆分大物件」,將單一大配置轉化為多個小配置,讓 Gen 0/1 GC 的高效壓縮機制發揮作用。BlockList<T> 等資料結構封裝了這份複雜性,是解決 LOH 碎片化的利器。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved