GC 是 .NET 執行環境的核心支柱,理解其運作原理、代碼分代策略、非托管資源處理模式,是寫出高效能、無記憶體洩漏程式的前提。本文從型別記憶體配置、GC 演算法、Dispose 模式到實戰測試代碼,完整建立記憶體管理知識體系。
| 類別 | 範例型別 | 資源本質 |
| 檔案/流 | FileStream, StreamReader, StreamWriter | 檔案控制代碼 |
| 資料庫 | SqlConnection, OdbcConnection, OleDbDataReader | 連線池、網路通訊端 |
| 圖形/GDI | Bitmap, Graphics, Pen, Brush, Font, Icon | GDI 物件控制代碼 |
| 網路 | Socket, TcpClient, HttpWebRequest | 通訊端控制代碼 |
| COM/Interop | Excel.Application, Word.Document | COM 參考計數 |
| 同步/執行緒 | Mutex, Semaphore, Timer, WaitHandle | 核心物件控制代碼 |
資源釋放雙軌制:Finalize vs Dispose
| 機制 | 觸發時機 | 決定性 | 效能影響 | 適用場景 |
| Finalizer (~ClassName) | GC 回收前 (非決定性) | ❌ 不確定 | 雙重 GC 週期、Finalizer 執行緒阻塞 | 防禦性程式設計、最後防線 |
| IDisposable.Dispose() | 程式碼明確呼叫 (決定性) | ✅ 確定 | 無額外 GC 負擔 | 所有非托管資源持有者 |
標準 Dispose 模式完整實作 (Microsoft 推薦)
public class ResourceHolder : IDisposable
{
private IntPtr _handle; // 非托管資源控制代碼
private Component _managed; // 托管資源 (也實作 IDisposable)
private bool _disposed = false; // 防重複釋放旗標
public ResourceHolder()
{
_handle = NativeMethods.OpenResource();
_managed = new Component();
}
// 公開 Dispose:決定性釋放
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 關鍵:從 Finalization Queue 移除,避免雙重清理
}
// 核心邏輯:disposing=true 來自 Dispose(),false 來自 Finalizer
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 釋放托管資源 (安全:其他物件尚未被 Finalize)
_managed?.Dispose();
}
// 釋放非托管資源 (無論 disposing 真偽都必須執行)
if (_handle != IntPtr.Zero)
{
NativeMethods.CloseHandle(_handle);
_handle = IntPtr.Zero;
}
_disposed = true;
}
// Finalizer:防禦性備援 (僅在 Dispose 未被呼叫時執行)
~ResourceHolder()
{
Dispose(false); // 僅清理非托管資源
}
// 方法防護:防止使用已釋放物件
public void DoWork()
{
if (_disposed) throw new ObjectDisposedException(nameof(ResourceHolder));
// 實際工作...
}
}
using 陳述式:語法糖保證釋放
// 等價於 try/finally 區塊
using (var resource = new ResourceHolder())
{
resource.DoWork();
}
// 編譯器產生:
// try { resource = new ResourceHolder(); resource.DoWork(); }
// finally { if (resource != null) resource.Dispose(); }
IL 驗證:using 與手寫 try/finally Dispose() 產生完全相同的 IL 指令碼。
實戰測試代碼:四種資源類別行為對比
// 1. 實作 IDisposable + using (最佳)
using (var obj = new WithDispose()) { obj.DoWork(); } // 立即釋放
// 2. 無 Finalize (純托管/安全資源) - 依賴 GC
var obj2 = new NoFinalize(); obj2.DoWork(); // 不確定時機回收
// 3. 僅 Finalizer (舊版模式) - 雙重 GC 開銷
var obj3 = new WithDestructor(); obj3.DoWork(); // 兩次 GC 才釋放
// 4. 錯誤:Finalizer 類別不能用 using (編譯錯誤)
// using (var obj4 = new WithDestructor()) { } // CS1674: 不實作 IDisposable
關鍵最佳實踐總結
- 持有非托管資源的類別必須實作 IDisposable
- 公開 Dispose() 呼叫 GC.SuppressFinalize(this) 移除 Finalization 開銷
- Finalizer 僅作防禦備援,不可替代 Dispose
- 使用 using 區塊 確保區塊結束時釋放 (即使拋出例外)
- 避免在 Finalizer 存取其他參考型別欄位 (可能已被回收)
- 大型物件 (>85KB) 分配謹慎,進入 LOH 避免頻繁 Gen 2 回收
- 弱引用 (WeakReference) 實作快取,允許 GC 回收記憶體壓力下的資料
記憶體管理不是「有 GC 就不用管」,而是理解 GC 行為、配合決定性釋放模式、設計友善 GC 的物件圖。這是 .NET 開發者從初級邁向資深的核心能力分水嶺。
- 上一篇文章: .NET 深度學習筆記 (4):深拷貝與淺拷貝完整解析——機制、實作與效能考量
- 下一篇文章: .NET 深度學習筆記 (2):C# 判斷空字串 4 種方法效能實測與 IL 深度剖析
Copyright ©
程式碼之家 All Rights Reserved