2010年5月30日 星期日

C#記憶體管理

C++ C#程式而言, 記憶體的配置區域大致可分成三種
  • Global區域 -若變數,函式或物件宣告成static,將被配置到此區域內(這類的記憶體,常被稱為全域變數(global variable), 此區域內的所有資料為同一個process共用; Global區域內的記憶體生命週期會維持到程式結束後, 才會被釋放.
  • Stack區域 - Stack區域是用來儲存區域變數(local variable), 記憶體配置的生命週期受到其區塊範圍 (block scope) 的影響,當進入區塊時,會自動配置記憶體, 離開此區塊,記憶體也會被釋放.
  • Heap區域- Heap是儲存動態配置的資料(例如:使用newmalloc()宣告), 此記憶體不會隨函式結束被釋放. 而且釋放方式會因不同程式語言而不同. 對於C/C++這類的程式, 記憶體釋放需要由程式員手動釋放(例如: 使用deletefree()); 而 JavaC# 程式, 記憶體釋放則由 VM(虛擬機)自動作處理, 也就是Garbage Collection的機制.
如果記憶體的釋放是交由 GC 負責控制, 程式員就不必擔心記憶體遺失或使用到錯誤記憶體等問題(可參考How to detect memory leak), 既然GC那麼方便, 那我們先來了解GC是如何運作?

GC 會追蹤 (tracing) 所有活著的物件, 如果不是活著的物件, 就會被標成 GC 該清除的垃圾.若變數為數值型別, 當變數超出有效範圍時, CLR會直接回收它在Stack上所佔用的空間, 若變數為參考型別, CLR會先回收它在Stack上佔用的空間, 再將Heap上的空間視為garbage, 等待GC回收. 若參考型別的變數在其有效範圍內重新初始化, 則原先所指向的物件亦會被視為garbage.
然而, 被視為garbage的物件, 並不會馬上就被GC回收, 而是根據GC內的演算法, 依變數被判定的generation而有不同.
下面為GC的測試程式:

C#程式碼 :

static void Main(string[] args)
{
Console.WriteLine("程式開始");
MyClass myClass = new MyClass();
myClass = null;
Console.WriteLine("程式結束");
}

執行結果,
GC會在程式結束後自動作記憶體回收:

[圖1] GC自動回收記憶體

雖然GC會幫程式人員自動清理記憶體, 但還是有可能發生, 要及時釋放某些記憶體(unmanged object)或資源時, 例如視窗控制代碼 (HWND), 資料庫連接等時, 該怎麼做?

C# 提供三種方式, 讓程式人員可手動釋放記憶體或資源:
(1) 呼叫GC.Collect()
使用GC.Collect()可強制系統嘗試回收可用記憶體的最大量. 但一般來說. 應該盡量避免呼叫GC.Collect()來回收記憶體, 因為在記憶體回收行程執行回收之前,它會暫停所有目前正在執行的執行緒. 如果在不必要的情況下呼叫 GC.Collect()太過頻繁的話, 反而會造成效能上的問題. 此方法使用範例如下:

C#檔

static void Main(string[] args)
{
Console.WriteLine("程式開始");
MyClass myClass = new MyClass();
myClass = null;
GC.Collect();
//因為GC回收時是在另一個thread
//所以使用WaitForPendingFinalizers等到確實回收後再繼續執行
GC.WaitForPendingFinalizers();
Console.WriteLine("程式結束");
}


執行結果

[圖2] 使用GC.Collect()強制回收記憶體

(2) 實作 Finalize
實作
Finalize 方法, 如同C++中的解構函式語法, 程式碼範例如下:

C#檔

public class MyClass
{
UnmanagedResource unmRes ;

public MyClass()
{
Console.WriteLine("建構 MyClass");
unmRes = new UnmanagedResource();
}

~MyClass()
{
/// 釋放unmanaged資源
unmRes.Release();

Console.WriteLine("解構 MyClass");
}
}

執行結果:

[圖3] 利用Finalize 回收記憶體

Finalize可用於釋放一些記憶體, 但執行的時機由GC控制, 因此我們無法預期Finalize()究竟會何時會被呼叫, 這樣如果我們有急迫的資源必須釋放,這時候怎麼辦?

(3)使用Dispose
若要明確地釋放資源和物件, 使用Dispose將是最好的方式.程式員呼叫的方式如下:
  • 明確地呼叫Dispose()
  • 使用using陳述式, 在離開using區塊時, 其Dispose()會被明確地呼叫
請注意, 使用 Dispose 雖然可以讓程式員明確地控制, 但也不能少用 Finalize 方法. 因為當程式員呼叫 Dispose 失敗, 則 Finalize 會提供備份, 以避免資源沒被釋放掉, 相關程式碼如下:

C#檔:

public class Memory : IDisposable
{
private bool isDisposed = false;

public Memory()
{
Console.WriteLine("建構 Memory");
}

~Memory()
{
Dispose(false);
Console.WriteLine("解構 Memory");
}

protected void Dispose(bool Diposing)
{
if (!isDisposed)
{
if (Diposing)
{
//Clean Up managed resources
}
//Clean up unmanaged resources
}
isDisposed = true;
}

public void Dispose()
{
Console.WriteLine("Dispose called!");
Dispose(true);
//藉由呼叫 GC.SuppressFinalize 方法來避免 Finalize 方法的執行
GC.SuppressFinalize(this);
}
}


執行結果:
Console.WriteLine("程式開始");
using (Memory memory = new Memory())
{

}
Console.WriteLine("程式結束");


[圖4] 利用Dispose回收記憶體

結 論 :
  • GC會自動管理Managed resource, 所以不需要程式員煩惱, 除非有用到 unmanaged resource 並需要釋放, 才會用Dispose/Finalize
  • Finalize() 是交給 GC 安排時間去處理, Dispose() 則讓使用者決定使用的時機
  • 除非必要時機, 否則不建議呼叫GC類別的Collect方法來強制回收記憶體
參考文章:
記憶體回收

.NET 的自動記憶體管理

C# garbage collection 筆記

.NET Framework記憶體回收機制

C# - 資源釋放的觀念整理

實作 Finalize 和 Dispose 以清除 Unmanaged 資源

2010年5月28日 星期五

How to detect memory leak

記憶體管理對C/C++程式開發是個很重要的議題, 在程式中配置記憶體很容易, 但什麼時間點該釋放記憶體, 就是個很麻煩的問題. 如果配置的記憶體不作釋放, 就會導致系統永遠無法取得那塊記憶體,而造成記憶體遺漏(Memory Leak), 若釋放不該釋放的記憶體, 程式運行到一半找不到對應的位址時, 也會造成程式當掉.上述的這些問題, 常常會發生在C/C++的初學者, 或已經習慣自動記憶體管理的程式員身上.

下面的步驟說明如何在C++的程式中, 偵測memory leak的發生.
  • 引用下列標頭檔
        #define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
  • 輸出記憶體洩漏資訊
    _CrtDumpMemoryLeaks();
  • 在Visual Studio中選擇Debug模式然後執行, 若程式中有發生記憶體遺漏, 其相關資訊將出現在輸出視窗中.
下面的程式範例中, 我們利用new配置了一個物件記憶體, 且不將其記憶體作釋放:


#define _CRTDBG_MAP_ALLOC

#include "stdafx.h"
#include "MyClass.h"
#include <stdlib.h>
#include <crtdbg.h>

int _tmain(int argc, _TCHAR* argv[])
{
CMyClass* pClass = new CMyClass();
//delete pClass;
pClass = NULL;
_CrtDumpMemoryLeaks();

return 0;
}


在輸出視窗中我們將觀看到記憶體遺漏的資訊如下:
Detected memory leaks!
Dumping objects ->
{86} normal block at 0x00397D98, 1 bytes long.
Data: < > CD
Object dump complete.
The program '[848] MemoryLeakTest.exe: Native' has exited with code 0 (0x0).


參考文章:
啟用記憶體遺漏偵測

在Visual C++使用內建功能偵測memory leak