本文源於論壇一個關於 GC.Collect() 不立即回收局部變量對象的疑問。作者通過對比 Debug/Release 模式下 IL 指令、JIT 編譯後的機器碼、Windbg 追蹤 GC 根掃描過程,實證驗證了 .NET GC 「從根引用掃描可達對象」的核心理論,並揭示 JIT 優化 (如寄存器分配、局部變量槽復用) 對對象生命週期的實際影響。
static tc gto; // 靜態根引用
public class tc { public int a = 99; ~tc() { a = -1; gto = this; } }
private void button1_Click(...) {
tc to = new tc();
GC.Collect(); GC.WaitForPendingFinalizers();
// breakpoint: to 是否已被終結?
}
觀察:Debug 下首次點擊析構函數不執行,第二次才執行;Release 下首次即執行。原因在於 JIT 對局部變量槽的不同處理。
| 模式 | 關鍵 IL 指令 | 含義 |
|---|---|---|
| Debug | stloc.0 | 將新對象引用存入局部變量槽 0 (to),槽在方法結束前保持存活 → 為 GC 根 |
| Release | pop | 新對象引用壓棧後立即彈出,**不存入局部變量槽** → 無根引用,GC 可立即回收 |
JIT_NewFast 返回引用在 EAX,隨即 GCInterface::CollectGeneration 內部覆蓋 EAX,對象無任何根引用 → 被回收,析構函數執行。[ebp-0Ch],GC 掃描棧帧發現根 → 對象存活。to.ToString(),JIT 為保留調用將引用存入 ESI (被調用者保存寄存器),GC 掃描到寄存器根 → 對象存活,行為與 Debug 一致。pop),導致對象比預期更早可回收。~Finalize()) 釋放關鍵資源,**請實現 IDisposable 並顯式 using/Dispose()**。GC.KeepAlive(obj) 可防止 JIT 過早回收正在使用的對象 (如 P/Invoke 併發場景)。dotnet-gcdump、PerfView、Visual Studio 內存分析器,而非斷點觀察。