2009年2月16日 星期一

C# Gargabe 觀念整理



CLR(Common Language Runtime)

:CLR自動配置與管理的記憶體,稱為Managed資源
:不受CLR管理的便被稱為Unmanaged資源

使用強制釋放資源
System.GC.Collect();

通知 .Net framework不要對這個object進行資源回收
System.GC.SuppressFinalize(this)

// ---以下引用 慮山佚城所寫的文章-- 留做個人以後查資料的記錄 --- //

在執行.NET的程式時,對於不同型別,CLR(Common Language Runtime)會在不同的地方分配資源空間。對於實值型別(Value Type),當宣告一個變數時,CLR會在Stack(堆疊)中配置一塊空間,設定該變數的值時,其值也直接存放於該空間中,如下圖:


而對於參考型別(Reference Type),CLR則在Stack中配置一塊存放記憶體位址的空間,在初始化該型別的實體時(ex: new),則在Heap(堆積)上配置該型別所需的空間,再將該空間的位址傳回給存放在Stack中的那塊空間,如下圖:

這些由CLR自動置與管理的記憶體,被稱為Managed資源;反之,不受CLR管理的便被稱為Unmanaged資源(ex: Stream、與資料庫的連結、COM物件……等)。
而對於資源的釋放,CLR則主要以GC來回收已用不到的記憶體空間,這些空間便被稱為garbage。(上述的記憶體配置及GC的運作可參考
MSDN: CLR的自動記憶體管理)
那麼garbage如何產生或被判定呢?
基本的原則是當該變數「不再有效」時,便會被視為garbage,具體的情況則包括超出該變數的有效範圍(ex:離開了對應的大括號的區域變數)、 將變數指定為null、重新指向其他物件(而原先指向的物件已無法被取得)、重新初始化…等,這時原先變數佔有的空間都會被CLR視為garbage而等待回收。
若變數為數值型別,則當其超出有效範圍時,CLR會直接回收它在Stack上所佔用的空間;若變數為參考型別,則CLR會先回收它在Stack上佔用的空間,而將Heap上的空間視為garbage,等待GC回收。若參考型別的變數在其有效範圍內重新初始化,則原先所指向的物件亦會被視為garbage。
然而,被視為garbage的變數,並不是馬上就被GC回收,而是根據GC內的演算法,依變數被判定的generation而有不同。在Managed Heap上的物件會被CLR分為三個generation:0、1、2,數字越大表示存活時間越長。這樣的設計乃是建構在「只壓縮部份的Heap會比一次壓縮整個Heap來得有效率」的事實上,因此將物件分成不同的層級,在回收時針對不同層級做回收,是.NET Framework考量效率後所採用的方法。
CLR會將新建立的物件擺在第0個generation,當GC進行gen 0的回收時,會將仍為有效的gen 0物件提昇至gen 1。而若gen 0的空間回收後仍不足以建立新物件,則GC會繼續檢查gen 1的物件,存留下來的gen 1物件會被提昇至gen 2(最高只到gen 2,故若gen 2被檢查後仍存活,只會停留在gen 2)。進行gen 1的回收時,也會進行gen 0回收;同理進行gen 2回收時,也會進行gen 1、gen 0的回收。在這樣的設計下,程式中常會應用到的區域物件與暫時物件因都屬於gen 0,故可確保被回收的頻率最高,對一般不會用到大量暫時物件的程式而言,不需擔心資源的浪費。
至目前為止,我們了解了Managed資源的回收機制,但還有至少二個問題殘留著:
「若是程式員希望在某個時間點確保Managed資源被回收,應該怎麼做?」
「若是類別中含有Unmanaged資源,又該如何釋放?」
對於這二個問題的回答,在這裡便開始要提到程式員如何撰寫明確釋放資源的函式了。在C++中,程式員撰寫類別的Destructor來達成類別資源的釋放,而在C#中,同樣有類似效果的函式有Finalize()與Dispose() ,而這二個函式也常令許多C#的初學者混淆,以下便解釋二者的差別:
前面提到Managed資源會自動被GC回收,但若類別中含有Unmanaged資源,則程式員除了撰寫該Unmanaged資源本身的釋放函式(如Destructor)之外,又該在何處確保這些釋放函式會被呼叫呢?答案就是C#類別的Finalize()函式中。然而,因Finalize()本身的存取層級是protected,所以在實作上,對於類別T,程式員是撰寫名為 「~T()」的函式(如同C++中的解構函式語法),該函式經過編譯後,編譯器便會產生類別T的Finalize()方法,並將函式的內容包在try 區塊中。
public class T
{
Unmgd un;
~T()
{
un.Release(); //假設Unmgd的釋放資源函式為Release();亦可呼叫~Unmgd()
}
}
然而,Finalize()的呼叫是不明確的,因為它只被GC給呼叫,亦即程式員只能預期GC在進行某個T的實體的回收時,會呼叫T.Finalize(),卻無法在程式員自己想要的特定位置呼叫Finalize()。另一方面,Finalize()也並非在GC對該物件進行第一次回收時便被呼叫,事實上,GC的第一次回收,僅是將該物件的參考移至FReachable Queue中,標示為不再使用,等到GC下一次的回收時,Finalize()才真正地被呼叫。從這些事實來看,程式員沒有辦法預期Finalize()究竟會在程式中的何處被呼叫,只能被動地等待GC去呼叫它。因此,為了讓程式員能明確執行Unmanaged資源釋放的工作,C#提供了另一個函式Dispose()。
Dispose()與Finalize()最大的不同,在於Dispose()是明確地在「程式員呼叫」與「using語句區塊結束時」二種情況下被呼叫的。也就是說,當程式員想確保某個類別T中的Unamaged資源會在某個特定位置被釋放掉時,他便可以實作IDisposable介面中的Dispose()函式;而為了提供程式員在操作常用的暫時物件上不會忘記呼叫Dispose(),C#中提供了using陳述式,確保在小括弧中的變數,在離開using區塊時,其Dispose()會被明確地呼叫。
using(T t = new T())
{
.........
} //<-此時T.Dispose()會被呼叫
當一個物件的Dispose()被呼叫後,亦即標示了這個物件為無效,則在第一次的GC回收行程中便會被回收到。值得一提的是,即使某個物件的Dispose()已經被呼叫過了,該物件的Finalize()仍然有可能再被GC給呼叫到,這是為了避免當Dispose()失敗時可能產生的資源浪費。然而,若Dispose()成功地執行了,則應該避免再讓GC去呼叫Finalize(),否則會造成程式效率的低落。因此在撰寫Dispose()函式時,必須記得在裡面加上一行GC.SuppressFinalize(this); 這可以告訴GC不需再去呼叫這個物件的Finalize()。

//------------------------------------------------------------------------------------------//

資料來源:
史帝芬心得筆記
http://home.so-net.net.tw/idealist/CS/Basic/Garbage.html
廬山佚城 http://sedc.pixnet.net/blog/post/14184015
Joel Lee http://dialog.bcse.info/archives/memory-leak-in-c-sharp





沒有留言:

張貼留言