在.NET中,由於有了垃圾回收機制的支持,對象的析構和以前C++有了很大的不同,這就要求程序員在設計類型的時候,充分理解.NET的機制,明確怎樣利用Dispose方法和Finalize方法來保證一個對象正確而高效地被析構。
我們知道,在.NET環境中,托管的資源都將由.NET的垃圾回收機制來釋放,而一些非托管資源則需要程序員手動地進行將他們釋放。.NET提供了主動和被動兩種釋放非托管資源的方式,即IDisposable接口的Dispose方法和類型自己的Finalize方法。
1.using的實現原理
在C#中,using語句提供了一個高效的調用對象Dispose方法的方式。對於任何IDispose接口的類型,都可以使用using語句,而對於那些沒有實現IDisposable接口的類型,使用using語句會導致一個編譯錯誤。
先來看下using語句的基本語法:
using (MemoryStream ms=new MemoryStream()) { //... }
在上面的代碼中,using語句的一開始定義了一個MemoryStream的對象,之后再整個語句塊中都可以使用ms,在using語句塊結束的時候,ms的Dispose方法將會被自動調用。using語句不僅免除了程序員輸入Dispose調用的代碼,它還提供了機制保證Dispose方法被調用,無論using語句塊順利執行結束,還是拋出一個異常。事實上,C#編譯器為using語句自動添加了try/finally塊,所以Dispose方法能夠保證被調用到,下面的兩端代碼經過編譯后內容將完全一樣:
using(MyDispose md=new MyDispose()) { md.DoWork(); }
等價於:
MyDispose md; try { md=new MyDispose(); md.DoWork(); } finally { md.Dispose(); }
在了解了using的實現原理之后,DebugLZQ提醒:要避免在使用using時時常犯的錯誤,那就是千萬不要試圖在using語句塊外初始化對象,如下面代碼所做的:
MyDispose md=new MyDispose(); using(md) { md.DoWork(); }
看上去似乎沒有任何問題,但是在多線程的程序中,上述代碼就會有隱患。試想當md被初始化后程序突然產生一個異常而中斷,那md對象中的非托管資源就沒有機會得到釋放,而這對系統來說危害是非常大的。所以在任何時候都應該在using語句塊中初始化需要使用的對象。
關於using詳細,請參考DebugLZQ博文:"using" in C#
2.Dispose調用時機
上面DebugLZQ介紹了Dispose方法,正是因為垃圾回收機制掩蓋了對象內存回收的時間,考慮到很多情況下程序員仍然希望在對象不再被使用時進行一些清理工作,所以.NET提供了IDisposable接口並且在其中定義了Dispose方法。
但是,我們要注意實現了Dispose方法不能得到任何有關釋放的保證,Dispose方法的調用依賴於類型的使用者(就是我們程序員),當類型被不恰當的使用時,Dispose方法將不會被調用。但是,using等語法的存在還是幫助了Dispose方法被調用。
廢話不多說,就這個就講這么多~
3.Finalize方法的機制
如1、2所說,由於Dispose方法的調用依賴於使用者,為了彌補這一缺陷,.NET同時提供了Finalize方法。Finalize方法通常被具有C++開發經驗的程序員稱為析構方法,但它的執行方法卻和傳統C++中的析構函數完全不同。Finalize方法在GC執行垃圾回收時調用,具體的機制是這樣的:
- 當每個包含Finalize方法的類型的實例對象被分配時,.NET會在一張特定的表中添加一個引用並且指向這個實例對象。方便起見起見就稱該表為“帶析構對象表”。
- 當GC執行並且檢測到一個不被調用的對象時,需要進一步檢查“帶析構對象表”來查看該對象類型是否具有Finalize方法,如果沒有則該對象被視為垃圾,如果存在Finalize方法,則把該對象的引用從“帶析構對象表”移動到另外一張表中,這里暫時稱它為“等待析構表”。並且該對象實例被視為仍然在被使用。
- CLR醬油一個單獨的線程負責處理“等待析構表”,其方法就是一次通過引用調用其中每個對象的Finalize方法,然后刪除引用,這是托管堆(DebugLZQ特別提醒,注意:.NET中的垃圾回收,就是釋放這個堆上不再被使用的內存對象(因為引用類型的對象存放在這個堆上)。注意區分堆棧,堆棧存放的是值類型對象和托管堆上引用類型對象的指針。)中的對象實例被認為處於不在被使用的狀態。
- 在下一個GC執行時,將釋放已經被調用Finalize方法的那些對象實例。
警告:Finalize方法應該只致力於快速而簡單地釋放非托管資源,並且盡可能快地返回。不正確的Finalize方法可能包含這樣的代碼:
- 沒有保護地寫文件日志
- 訪問數據庫
- 訪問網絡
- 當前對象被付給某個存活的引用
當Finalize方法試圖訪問文件系統、數據庫系統或者網絡時,將會有資源爭用和等待的潛在危險。試想一個不斷嘗試訪問離線數據庫的Finalize方法,將會在長時間內不會反回,這不僅影響了對象本省的釋放,也使得排在Finalize方法隊列中的所有后續對象得不到釋放,這個連鎖反應將很快造成內存耗盡(結果是死機,最近DebugLZQ在醫生站項目中就曾遇到了這個情況!!!)。
而另外一種危險的代碼是在Finalize方法中把對象自身又賦值給另外一個存活的引用,這時對象內的一部分資源已經被釋放了,而另外一部分則沒有,這樣一個對象再被激活后,將導致不可預知的后果。
4.正確地使用Dispose和Finalize方法
一步一步,寫的越來越深,不知道你有沒有看懂,但DebugLZQ還得繼續,因為問題沒有說清楚~
從安全性上來講,Finalize方法確實比Dispose方法更加安全,因為它由CLR保證調用。
但是性能方面,Finalize方法卻要差得多!!!
這一點DebugLZQ簡單做一個解釋吧:和.NET 垃圾回收機制中3代機制有關,根據.NET的垃圾回收機制,0代,1代,2地的初始分配空間分別為256KB,2MB和10MB。當某個實例在GC執行時被發現仍然在使用,它將被移動到下一代上。並且並不是每次垃圾回收都會回收3代的所有對象,越小的代擁有者越大的回收機會。CLR的基本算法是:執行N代的0代垃圾回收,才會執行一次1代的垃圾回收;執行N次的1代的垃圾回收,才會執行一次2代的垃圾回收。相對於0代的快速釋放,1代和2代的對象具有較少的回收機會,結合3中DebugLZQ講述的Finalize機制,你應該能想明白,為什么Finalize會大幅度地影響性能了。
5.一個近乎完美的Finalize配合Dispose的設計模板
DebugZLQ寫到這里,各位博友應該知道,正確的類型設計是把Finalize方法作為Dispose方法的后備,只有在使用者沒有調用Dispose方法的情況下,Finalize方法才被視為需要執行。下面給出一個正確高效的設計模板,DebugLZQ建議各位博友牢記這個模板並且套用到每一個需要Dispose和Finalize方法的類型上去。
using System; using System.Collections.Generic; using System.Text; namespace PerfectFinalizeDispose { //DebugLZQ //http://www.cnblogs.com/DebugLZQ public class FinalizeDisposeBase : IDisposable { // 標記對象是否已被釋放 private bool _disposed = false; // Finalize方法: ~FinalizeDisposeBase() { Dispose(false); } // 這里實現了IDispose中的 Dispose方法 public void Dispose() { Dispose(true); //告訴GC此對象的Finalize方法不再需要調用 GC.SuppressFinalize(this); } //在這里做實際的析構工作 //申明為虛方法以供子類在有必要時重寫 protected virtual void Dispose(bool isDisposing) { // 當對象已經被析構時,不再執行 if (_disposed) return; if (isDisposing) { //在這里釋放托管資源 //只在用戶調用Dispose方法時執行 } //在這里釋放非托管資源 //標記對象已被釋放 _disposed = true; } } public sealed class FinalizeDispose : FinalizeDisposeBase { private bool _mydisposed = false; protected override void Dispose(bool isDisposing) { // Don't dispose more than once. if (_mydisposed) return; if (isDisposing) { //在這里釋放托管的並且在這個類型中申明的資源 } //在這里釋放非托管的並且在這個類型中申明的資源 //調用父類的Dispose方法來釋放父類中的資源 base.Dispose(isDisposing); // 設置子類的標記 _mydisposed = true; } static void Main() { } } }
以上為一個近乎完美的Finalize配合Dispose的設計模板,其中有幾點需要各位博友注意:
- 真正做釋放工作的只是virtual的受保護的Dispose方法,事實上這個方法的名字並不重要,僅僅是為了通用和更好的理解,所以命名它為Dispose。
- 虛方法Dispose需要接受一個bool型的參數,主要用於區分調用方式類型的使用者還是.NET的垃圾回收。前者通過IDisposable的Dispose方法,而后者通過Finalize方法。兩者的區別是通過Finalize方法釋放資源時不再釋放或使用對象中的托管資源,這是因為這時的對象已經處於不被使用的狀態,很有可能其中的托管資源已經釋放掉了。
- 在IDisposable的Dispose方法的實現中通過GC.SupressFinalize()方法來告訴.NET此對象在被回收時不需要調用Finalize方法,這一句是改善性能的關鍵,記住實現Dispose方法的本質目的就是避免所有釋放工作都在Finalize方法中進行。
- 子類型必須定義自己的釋放標記來標明子類中的資源是否已經被釋放,同時子類的虛方法Dispose方法也只需要釋放自己新定義的資源。
- 確保在虛方法Dispose中做的都是釋放工作,有些邏輯上的結束工作需要反復斟酌的,以防止一個簡單的賦值語句使對象再度存活。
6.結束語
最近工作很忙,時間變得越來越寶貴,拿出時間來胡亂侃侃這個淺談那個,說實話挺不容易,寫作初衷是猶自對.NET的熱愛,目的是給各位博友分享一種對.NET的認識、理解,所以轉載請注明出自博客園DebugLZQ。
當然人非聖賢,博文中可能存在一些說法欠妥的地方,歡迎批評指正~
最后,請點擊下面的綠色通道,關注DebugLZQ,共同交流、共同進步~