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

偵錯 .NET 記憶體洩漏:發現與預防托管代碼中的記憶體洩漏

     編輯:關於.NET

托管代碼也會洩漏記憶體?

許多開發者以為 GC 包辦一切,洩漏不可能發生。事實上:

  • GC 僅管理托管堆;.NET 應用大量使用非托管記憶體 (CLR 內部結構、P/Invoke、COM Interop)。
  • 編程錯誤可能阻礙 GC 正常工作,導致托管物件無法回收 (邏輯洩漏)。

開發者仍需持續監控記憶體使用,確保應用高效穩定。

.NET 應用的三種記憶體

類型用途管理者
堆疊區域變數、參數、返回值、臨時值自動 (方法返回釋放),GC 掃描引用
非托管堆CLR 執行階段結構、方法表、JIT 代碼、P/Invoke/COM 配置開發者/CLR 手動管理
托管堆托管物件分配區分代壓縮 GC (Gen 0/1/2 + LOH)

偵測洩漏類型:PerfMon 計數器

  • Process/Private Bytes:進程專用總記憶體。
  • .NET CLR Memory/# Bytes in All Heaps:托管堆總大小 (Gen 0/1/2 + LOH)。
  • .NET CLR LocksAndThreads/# of current logical Threads:邏輯執行緒數。

判斷邏輯:

  • 邏輯執行緒數暴增 → 執行緒堆疊洩漏。
  • Private Bytes 增、Managed Heap 不變 → 非托管堆洩漏。
  • 兩者同增 → 托管堆物件累積 (邏輯洩漏或中年危機)。

堆疊洩漏:執行緒未正確終止

預設執行緒堆疊 1 MB。若建立背景執行緒卻未正常結束 (如 Thread.Join() 等待自己),每建立一個洩漏 1 MB。

// 錯誤範例:執行緒自我 Join 導致永久阻塞
Thread t = new Thread(() => Thread.CurrentThread.Join());
t.Start(); // 堆疊永不釋放

GC 僅回收 Thread 物件,不回收 OS 執行緒堆疊。必須讓執行緒函數正常返回或呼叫 Abort() (不推薦)。

非托管堆洩漏常見原因

1. 與非托管代碼互操作 (P/Invoke、COM)

GC 看不見非托管記憶體。若托管代碼呼叫非托管 API 配置記憶體卻未釋放 → 洩漏。需逐步檢查呼叫前後記憶體變化,用傳統偵錯工具定位非托管端洩漏。

2. 終結器被阻塞

CLR 僅有一個終結器執行緒。若某終結器卡住 (如嘗試連離線資料庫不返回),後續終結器永不執行 → 非托管資源洩漏。AppDomain 卸載時 CLR 會嘗試執行所有終結器,但有超時機制,超時則強制終止進程。ASP.NET/SQL Server 宿主環境下,AppDomain 回收不代表進程結束,洩漏記憶體持續累積。

.NET 2.0 引入關鍵終結器 (Critical Finalizer),保證 AppDomain 關閉時獲得執行機會。

3. 組件洩漏 (動態代碼生成)

組件載入後僅能透過卸載 AppDomain 釋放。動態生成代碼 (System.CodeDom、XmlSerializer 多載建構函式) 若反覆生成新組件,舊組件常駐記憶體 → 非托管堆 (JIT 代碼、方法表) 與托管堆 (靜態欄位) 雙重洩漏。

解決方案:

  • 動態組件載入至子 AppDomain,需更新時卸載子域重建。
  • .NET 2.0 DynamicMethod (輕量級代碼生成):MSIL 直接分配於托管堆,無引用即可被 GC 回收。
  • XmlSerializer 洩漏:使用帶 XmlRootAttribute 建構函式會每次生成新組件 → 改用類別屬性 [XmlRoot("Name")] 或自行快取 XmlSerializer 實例。

「洩漏」托管堆:GC 效能陷阱

1. 大型物件堆 (LOH) 碎片

>=85,000 bytes 物件進入 LOH,不壓縮。頻繁分配/釋放大陣列、大字串 → 碎片導致記憶體使用率虛高。對策:重用緩衝區、用 StringBuilder 減少臨時大字串、CLRProfiler 視覺化分析。

CLRProfiler LOH 視圖

2. 不必要的根引用

長時間持有大型物件圖參考 (如遠端呼叫前建立大圖,呼叫期間發生 GC → 物件倖存升代)。對策:縮短根引用生命週期,遠端呼叫前釋放參考。

3. 中年危機

物件本應短命卻倖存升代 (Gen 1/2),導致低成本 Gen 0 GC 轉為高成本全堆掃描。常見原因:

  • 空終結器:~Foo() {} 使物件進入終結隊列,強制升代。原則:無非托管資源不寫終結器;有非托管資源用 SafeHandle 包裝 + IDisposable 模式。
  • 阻塞呼叫前持有物件:DB 查詢、Web Service 呼叫、Thread.Sleep 等期間發生 GC → 物件升代。
  • 事件處理常式形成隱含參考:ASP.NET Cache.Add(..., callback) 中使用實例方法回呼 → Cache (根) → 委派 → Page (this) → 頁面所有臨時物件至少存活至快取過期 (如 5 分鐘) → 大量升代。修正:回呼改為 static 方法切斷 this 參考。
// 問題代碼
Cache.Add(key, obj, ..., new CacheItemRemovedCallback(OnCacheItemRemoved)); // OnCacheItemRemoved 為實例方法
// 修正
Cache.Add(key, obj, ..., new CacheItemRemovedCallback(StaticOnCacheItemRemoved)); // 靜態方法

總結

.NET 減輕記憶體管理負擔,但不代表可忽視。持續監控、理解 GC 機制、避免非預期根引用、正確處理非托管資源、善用工具分析,才能構建高效穩定的應用。良好的軟體工程實踐永遠不可或缺。

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