2011年9月7日 星期三

C#記憶體與資源的釋放與管理

研讀了好多好多文章,然後得到一些心得
如果有人看了覺得不對,麻煩歡迎您告訴我


首先,看看記憶體回收機制
這兩篇文章中提到


從 Managed 堆積 (Heap) 為該物件配置記憶體。只要在 Managed 堆積中有位址空間可用,Runtime 就會繼續為新物件配置記憶體。但是,記憶體畢竟不是無限的。到最後,記憶體回收行程還是必須進行回收以釋放某些記憶體。記憶體回收行程的最佳化引擎會依據所做的配置決定進行回收的最佳時機。當記憶體回收行程進行回收時,它會檢查 Managed 堆積中應用程式已不再使用的物件,並且執行必要作業以回收它們的記憶體。

在 Managed 執行階段環境中,記憶體回收堆積 (Heap) 會管理所有類別物件。這個堆積會監視物件的存留期 (Lifetime),並且只有在程式的任何部分都不參考物件時才加以釋出。這樣可以確保這些物件不會遺漏 (Leak) 記憶體,並可確保對物件的參考永遠有效。


這樣我的心得是只要程式當中的變數或成員在他生命週期結束的時候,在GC(Garbage Collection,C#的資源回收車)定時巡邏的時候,就有機會被釋放出來(據說不會很快釋放,要巡邏好幾趟確認)。而有別於C++的delete指令可以直接把記憶體釋放,C#以這個資源回收機制,來達到安全、效能、並且讓開發人員無須擔憂記憶體管理。

但我們經常在網路上看到不少文章,要我們對資源進行釋放


using 陳述式是一個非常基本的 C# 語法,相信在實務開發上經常會使用到,如果沒使用過的人可能代表你對 .NET 的記憶體管理的 Sense 不太夠,很有可能寫出資源耗盡的 .NET 程式碼。
雖然 .NET 有內建強大的記憶體管理機制(GC),但開發人員還是不能完全依賴 .NET 來處理一些無法釋放的資源,例如:Handles, Unmanaged Resources, …。

C#提供了與C++一樣語法的解構子,當object消失時,.Net framework 會執行解構子,我們可於此時加入程式進行資源回收,與Java相同的是,什麼時候object會消失? 這由VM決定,並非在object變成null時,VM即會立刻執行解構子並回收object


看了都會覺得害怕,擔心自己寫的程式會不斷的耗盡資源,而且還真的有發生。但是詳細看一下,他們所指的,大多是指:無法釋放的資源(Unmanaged Resources),好,再來看看是否真的如此。先從解構子來看起吧,根據官方文件說法,是這樣的:



程式設計人員無法控制呼叫解構函式的時間,因為這是由記憶體回收行程所決定的。記憶體回收行程會檢查不再被應用程式使用的物件。如果它將物件視為符合解構資格,便會呼叫解構函式 (如果存在),並且回收用來儲存該物件的記憶體。當程式結束時,解構函式也會被呼叫。

☆解構函式是用來解構類別的執行個體。
☆結構中無法定義解構函式。它們只能與類別一起使用。
☆一個類別只能有一個解構函式。
☆解構函式不能被繼承或多載。
☆解構函式不能被呼叫。它們會被自動叫用。
☆解構函式不使用修飾詞或參數。
☆解構函式會隱含呼叫物件之基底類別 (Base Class) 上的 Finalize


也就是說,不是在你把物件設定為NULL或是生命週期一結束,就會呼叫解構子,要等GC認定符合資格才行,那該信任GC嗎?我們會不會很快就把資源耗盡?我們繼續參考以下非官方文章:



基本的原則是當該變數「不再有效」時,便會被視為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考量效率後所採用的方法。程式中常會應用到的區域物件與暫時物件因都屬於gen 0,故可確保被回收的頻率最高,對一般不會用到大量暫時物件的程式而言,不需擔心資源的浪費


也就是說一般大多情況我們是該相信GC的,但是如果我們的程式需要用到很多資源呢?繼續看下去


「若是程式員希望在某個時間點確保Managed資源被回收,應該怎麼做?」「若是類別中含有Unmanaged資源,又該如何釋放?」對於這二個問題的回答,在這裡便開始要提到程式員如何撰寫明確釋放資源的函式了。在C++中,程式員撰寫類別的Destructor來達成類別資源的釋放,而在C#中,同樣有類似效果的函式有Finalize()與Dispose()。Dispose()與Finalize()最大的不同,在於Dispose()是明確地在「程式員呼叫」與「using語句區塊結束時」二種情況下被呼叫的。也就是說,當程式員想確保某個類別中的Unamaged資源會在某個特定位置被釋放掉時,他便可以實作IDisposable介面中的Dispose()函式而為了提供程式員在操作常用的暫時物件上不會忘記呼叫Dispose(),C#中提供了using陳述式,確保在小括弧中的變數,在離開using區塊時,其Dispose()會被明確地呼叫。當一個物件的Dispose()被呼叫後,亦即標示了這個物件為無效,則在第一次的GC回收行程中便會被回收到。值得一提的是,即使某個物件的Dispose()已經被呼叫過了,該物件的Finalize()仍然有可能再被GC給呼叫到,這是為了避免當Dispose()失敗時可能產生的資源浪費。然而,若Dispose()成功地執行了,則應該避免再讓GC去呼叫Finalize(),否則會造成程式效率的低落。


也就是說資源的釋放,可以交給GC,如果要自己來釋放,就要實作Dispose()
而關於Dispose()的實作方式,就參考這兩篇文章了




Unmanaged Resources,或是你想要早點自己釋放的Managed Resources寫在Dispose(),就行了。



綜合心得:

1.如果僅用了Managed資源,GC可以自動幫我們釋放記憶體,如果我們需要自己提早釋放,則可以實作出Dispose()函式。
2.如果使用了Unmanaged資源,一定要寫Dispose()函式,才能自己釋放,否則只能等程式停止才會釋放。
3.官方說寫Destructor()但是裡面如果是空的,對Managed Resource來說是沒有幫助的,反而減低效能,所以要寫出Destructor(),就要把裡面的內容實作出來才有意義。
4.Dispose()的內容,參考MSDN的作法,寫一個Dispose( bool )的函式,如果參數是true,是call by program (程式設計師),就釋放Unmanaged ResourcesManaged Resources。而如果是false,這是call by C#(C#機制自動呼叫的),這時候,因為Managed Resources已經被釋放了,所以就避開,不要再次呼叫Managed Resources的Dispose(),不然會多耗費效能的。

2 則留言: