CLR 的垃圾回收器 (GC) 讓開發者免於手動記憶體管理,但實際開發中關於「何時該手動釋放」、「GC 能否完全信賴」的疑慮從未停止。本文透過實測代碼驗證核心行為,釐清托管/非托管資源邊界,給出生產環境可落地的資源管理策略。
| 概念 | 定義 | GC 管理 | 範例 |
|---|---|---|---|
| 托管資源 | 純 .NET 物件佔用的記憶體 | ✅ 完全自動 | List<T>、String、DataTable、自訂類別實例 |
| 非托管資源 | 作業系統核心物件控制代碼 | ❌ GC 僅回收包裝物件,不會釋放底層控制代碼 | 檔案控制代碼、資料庫連線、GDI 物件、COM 介面、通訊端 |
| 包裝類別 | .NET Framework 封裝非托管資源的類別 | ⚠️ GC 回收包裝類別實例,但不保證及時釋放底層資源 | FileStream、SqlConnection、Bitmap、Socket |
// WinForms 測試表單
private void btnOpenFile_Click(object sender, EventArgs e)
{
// 僅建立 FileStream,未 Dispose、未 Close
FileStream fs = new FileStream(@"C:\DisposeTest.txt", FileMode.Open);
// fs 變數離開作用域,成為可回收
}
private void btnForceGC_Click(object sender, EventArgs e)
{
GC.Collect(); // 強制觸發 GC
GC.WaitForPendingFinalizers(); // 等待 Finalizer 執行
}
FileStream 等包裝類別有 Finalizer,GC 時會呼叫 Dispose(false) 釋放檔案控制代碼| 類別 | 必須 Dispose/Close | 理由 |
|---|---|---|
| 資料庫連線 | ✅ SqlConnection, OracleConnection | 連線池耗盡、資料庫端閒置連線佔用 |
| 檔案/流 | ✅ FileStream, StreamReader/Writer | 檔案鎖定、獨佔存取衝突 |
| GDI 物件 | ✅ Graphics, Bitmap, Pen, Brush, Font | GDI 物件表上限 (預設 10,000/程序),洩漏導致繪圖失敗 |
| 網路 | ✅ Socket, TcpClient, HttpWebResponse | 通訊端耗盡、TIME_WAIT 狀態堆積 |
| COM 物件 | ✅ Excel/Word Interop, RCW | 參考計數不歸零,伺服器進程不退出 |
| 同步物件 | ✅ Mutex, Semaphore, EventWaitHandle | 核心物件洩漏影響系統穩定 |
| DataSet/DataTable | ❌ 通常不需要 | 純托管記憶體,GC 自動回收 (除非極大觸發 LOH) |
| 物件 | 實作 IDisposable | 建議 |
|---|---|---|
| SqlCommand | 否 | 無需 Dispose (內部無非托管資源) |
| SqlDataReader | 是 | 必須 (持有資料庫連線、TDS 緩衝區) |
| SqlDataAdapter | 否 (繼承 Component) | 通常不需,若元件容器管理則自動 |
| DataSet/DataTable | 否 | 純托管,GC 管理 |
A:完全不是。GC 負責記憶體回收 (佔 90%+ 配置),Dispose 負責稀缺系統資源歸還。兩者分工不同、互補。良好設計:using 區塊決定性釋放非托管資源,GC 自動回收記憶體,雙管齊下。
// 1. 基本 using (最常用)
using (var conn = new SqlConnection(cs)) { conn.Open(); ... }
// 2. 多資源巢狀 using
using (var conn = new SqlConnection(cs))
using (var cmd = conn.CreateCommand())
using (var reader = cmd.ExecuteReader())
{
while (reader.Read()) { ... }
}
// 3. 傳統 try/finally (需相容舊版/複雜邏輯)
var conn = new SqlConnection(cs);
try { conn.Open(); ... }
finally { conn?.Dispose(); }
// 4. 類別實作 IDisposable (持有非托管資源時)
public class MyRepository : IDisposable
{
private readonly SqlConnection _conn;
public MyRepository() => _conn = new SqlConnection(cs);
public void Dispose() => _conn?.Dispose(); // 鏈式釋放
}
// 5. 進階:SafeHandle 替代 IntPtr (Framework 2.0+)
public class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeFileHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { SetHandle(handle); }
protected override bool ReleaseHandle() => NativeMethods.CloseHandle(handle);
}
!DumpHeap -stat、!GCRoot 找出洩漏根因.NET CLR Memory 計數器監控 Gen 0/1/2 收集頻率、LOH 大小資源管理的核心原則:**托管記憶體交給 GC,非托管資源用 using 決定性釋放,Finalizer 只做最後防線**。堅持此原則,.NET 應用將不再有資源洩漏噩夢。