前面一篇文章介紹了垃圾回收的基本工做原理,垃圾回收器並非能夠管理內存中的全部資源。對於全部的託管資源都將有.NET垃圾回收機制來釋放,可是,對於一些非託管資源,咱們就須要本身編寫代碼來清理這類資源了。程序員
其實在C#開發中,大部分資源均可以經過.NET垃圾回收機制進行回收,只用當咱們使用非託管資源(原始的操做系統文件句柄,原始的非託管數據庫鏈接,非託管內存等等)的時候,咱們才須要實現本身的資源清理代碼。數據庫
.NET提供了兩種釋放非託管資源的方式,類型本身的Finalize方法和IDisposable接口的Dispose方法。app
下面就來看看這兩個跟垃圾回收相關的方法。ide
在.NET的基類System.Object中,定義了名爲Finalize()的虛方法,這個方法默認什麼都不作。函數
咱們能夠爲自定義的類型重寫Finalize方法,在該方法中加入必要的非託管資源清理邏輯。當要從內存中刪除這個類型的對象時,垃圾回收器會調用對象的Finalize方法。因此,不管.NET進行一次自發的垃圾回收,仍是咱們經過GC.Collect()進行強制垃圾回收,Finalize方法老是會被調用。另外,當承載應用程序的AppDomain從內存中移除時,一樣會調用Finalize方法。性能
假設咱們如今有一個使用非託管資源的類型,那麼咱們就須要重寫Finalize方法來進行非託管資源的清理,可是當經過下面的方式重寫Finalize方法的時候,咱們會獲得一個編譯錯誤。this
class MyResourceWrapper { protected override void Finalize() { } }
其實,當咱們想要重寫Finalize方法時,C#爲咱們提供了(相似C++)析構函數語法(C#終結器)來重寫該方法。C#終結器和構造函數語法相似,方法名稱都和類型名稱同樣;不一樣的是,終結器具備~前綴,而且不能使用訪問修飾符,不接受參數,也不能重載,因此一個類只能有一個終結器。spa
class MyResourceWrapper { ~MyResourceWrapper() { Console.WriteLine("release unmanaged resources"); Console.Beep(); } }
之因此C#只支持這種方式進行Finalize方法的重寫,是由於C#編譯器會爲Finalize方法隱式地加入一些必需的基礎代碼。下面就是咱們經過ILSpy查看到了IL代碼,Finalize方法做用域內的代碼被放在了一個try塊中,而後無論在try塊中是否遇到異常,finally塊保證了Finalize方法老是可以被執行。操作系統
.method family hidebysig virtual instance void Finalize () cil managed { // Method begins at RVA 0x2050 // Code size 31 (0x1f) .maxstack 1 .try { IL_0000: nop IL_0001: ldstr "release unmanaged resources" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: call void [mscorlib]System.Console::Beep() IL_0011: nop IL_0012: nop IL_0013: leave.s IL_001d } // end .try finally { IL_0015: ldarg.0 IL_0016: call instance void [mscorlib]System.Object::Finalize() IL_001b: nop IL_001c: endfinally } // end handler IL_001d: nop IL_001e: ret } // end of method MyResourceWrapper::Finalize
當咱們執行下面代碼時,咱們就能夠聽到系統蜂鳴聲,像咱們前面介紹的同樣AppDomain被移除內存,類型終結器將被調用。線程
static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); }
Finalize的工做機制仍是比較複雜的,這裏只是簡單的介紹,更多的原理你們能夠本身網上查查。
當在託管堆上分配對象空間時,運行庫會自動肯定該對象是否提供一個自定義的Finalize方法。若是是這樣,對象被標記爲可終結的,同時一個指向這個對象的指針被保存在名爲終結隊列的內部隊列中。終結隊列是一個由垃圾回收器維護的表,它指向每個在從堆上刪除以前必須終結的對象。
當垃圾回收器肯定到了從內存中釋放一個對象的時間時,它檢查終結隊列上的每個項,並將對象從堆上覆制到另外一個稱做終結可達表(finalization reachable table的託管結構上。此時,下一個垃圾回收時將產生另一個線程,爲每個在可達表中的對象調用Finalize方法。所以,爲了真正終結一個對象,至少要進行兩次垃圾回收。
從上面能夠看到,Finalize方法的調用是至關消耗資源的。Finalize方法的做用是保證.NET對象可以在垃圾回收時清理非託管資源,若是建立了一個不使用非託管資源的類型,實現終結器是沒有任何做用的。因此說,若是沒有特殊的需求應該避免重寫Finalize方法。
當垃圾回收生效時,能夠利用終結器來釋放非託管資源。然而,不少非託管資源都很是寶貴(如數據庫和文件句柄),因此它們應該儘量快的被清除,而不能依靠垃圾回收的發生。除了重寫Finalize以外,類還能夠實現IDisposable接口,而後在代碼中主動調用Dispose方法來釋放資源。
看一個例子:
class MyResourceWrapper:IDisposable { public void Dispose() { Console.WriteLine("release resources with Dispose"); Console.Beep(); } } class Program { static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); mr.Dispose(); } }
一樣,當咱們顯示的調用Dispose方法的時候,能夠聽到系統的蜂鳴聲。
注意,經過Dispose進行資源的釋放也是有潛在的風險的,由於Dispose方法須要被程序員顯示的調用,若是代碼中漏掉了Dispose的調用或者在Dispose調用以前產生了異常從而沒有指定Dispose,那麼有些資源可能就一直留在內存中了。
因此咱們應該使用下面的方式保證Dispose方法能夠被調用到:
static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); try { //do something wiht mr object } finally { mr.Dispose(); } }
可是,每次編寫Dispose的代碼都使用try塊會以爲很麻煩,還好C#中,咱們能夠重用using關鍵字來簡化Dispose的調用。
在C#中,using語句提供了一個高效的調用對象Dispose方法的方式。對於任何IDispose接口的類型,均可以使用using語句,而對於那些沒有實現IDisposable接口的類型,使用using語句會致使一個編譯錯誤。
static void Main(string[] args) { using (MyResourceWrapper mr = new MyResourceWrapper()) { //do something with mr object } }
在using語句塊結束的時候,mr實例的Dispose方法將會被自動調用。using語句不只免除了程序員輸入Dispose調用的代碼,它還保證Dispose方法被調用,不管using語句塊順利執行結束,仍是拋出一個異常。事實上,C#編譯器爲using語句自動添加了try/finally塊。咱們能夠看看using的IL代碼:
.try { IL_0007: nop IL_0008: nop IL_0009: leave.s IL_001b } // end .try finally { IL_000b: ldloc.0 IL_000c: ldnull IL_000d: ceq IL_000f: stloc.1 IL_0010: ldloc.1 IL_0011: brtrue.s IL_001a IL_0013: ldloc.0 IL_0014: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0019: nop IL_001a: endfinally } // end handler
從前面的介紹瞭解到,Finalize能夠經過垃圾回收進行自動的調用,而Dispose須要被代碼顯示的調用,因此,爲了保險起見,對於一些非託管資源,仍是有必要實現終結器的。也就是說,若是咱們忘記了顯示的調用Dispose,那麼垃圾回收也會調用Finalize,從而保證非託管資源的回收。
其實,MSDN上給咱們提供了一種很好的模式來實現IDisposable接口來結合Dispose和Finalize,例以下面的代碼:
class MyResourceWrapper:IDisposable { private bool IsDisposed=false; public void Dispose() { Dispose(true); //tell GC not invoke Finalize method GC.SuppressFinalize(this); } protected void Dispose(bool Disposing) { if(!IsDisposed) { if(Disposing) { //clear managed resources } //clear unmanaged resources } IsDisposed=true; } ~MyResourceWrapper() { Dispose(false); } }
在這個模式中,void Dispose(bool Disposing)函數經過一個Disposing參數來區別當前是不是被Dispose()調用。若是是被Dispose()調用,那麼須要同時釋放託管和非託管的資源。若是是被終結器調用了,那麼只須要釋放非託管的資源便可。Dispose()函數是被其它代碼顯式調用並要求釋放資源的,而Finalize是被GC調用的。
另外,因爲在Dispose()中已經釋放了託管和非託管的資源,所以在對象被GC回收時再次調用Finalize是沒有必要的,因此在Dispose()中調用GC.SuppressFinalize(this)避免重複調用Finalize。一樣,由於IsDisposed變量的存在,資源只會被釋放一次,多餘的調用會被忽略。
因此這個模式的優勢能夠總結爲:
本文介紹了.NET垃圾回收中兩個相關的方法:Dispose和Finalize。Finalize的目的是用於釋放非託管的資源,而Dispose是用於釋放全部資源,包括託管的和非託管的。
Dispose須要在代碼中進行顯示的調用,而Finalize則是由垃圾回收自動調用,爲了更有效的結合Dispose和Finalize,文中還介紹了MSDN中給出的實現IDisposable接口的一個模式。