.NET垃圾回收 – 非託管資源

前面一篇文章介紹了垃圾回收的基本工做原理,垃圾回收器並非能夠管理內存中的全部資源。對於全部的託管資源都將有.NET垃圾回收機制來釋放,可是,對於一些非託管資源,咱們就須要本身編寫代碼來清理這類資源了。程序員

其實在C#開發中,大部分資源均可以經過.NET垃圾回收機制進行回收,只用當咱們使用非託管資源(原始的操做系統文件句柄,原始的非託管數據庫鏈接,非託管內存等等)的時候,咱們才須要實現本身的資源清理代碼。數據庫

.NET提供了兩種釋放非託管資源的方式,類型本身的Finalize方法和IDisposable接口的Dispose方法。app

下面就來看看這兩個跟垃圾回收相關的方法。ide

Finalize方法

在.NET的基類System.Object中,定義了名爲Finalize()的虛方法,這個方法默認什麼都不作。函數

咱們能夠爲自定義的類型重寫Finalize方法,在該方法中加入必要的非託管資源清理邏輯。當要從內存中刪除這個類型的對象時,垃圾回收器會調用對象的Finalize方法。因此,不管.NET進行一次自發的垃圾回收,仍是咱們經過GC.Collect()進行強制垃圾回收,Finalize方法老是會被調用。另外,當承載應用程序的AppDomain從內存中移除時,一樣會調用Finalize方法。性能

重寫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的工做機制仍是比較複雜的,這裏只是簡單的介紹,更多的原理你們能夠本身網上查查。

當在託管堆上分配對象空間時,運行庫會自動肯定該對象是否提供一個自定義的Finalize方法。若是是這樣,對象被標記爲可終結的,同時一個指向這個對象的指針被保存在名爲終結隊列的內部隊列中。終結隊列是一個由垃圾回收器維護的表,它指向每個在從堆上刪除以前必須終結的對象。

當垃圾回收器肯定到了從內存中釋放一個對象的時間時,它檢查終結隊列上的每個項,並將對象從堆上覆制到另外一個稱做終結可達表(finalization reachable table的託管結構上。此時,下一個垃圾回收時將產生另一個線程,爲每個在可達表中的對象調用Finalize方法。所以,爲了真正終結一個對象,至少要進行兩次垃圾回收。

從上面能夠看到,Finalize方法的調用是至關消耗資源的。Finalize方法的做用是保證.NET對象可以在垃圾回收時清理非託管資源,若是建立了一個不使用非託管資源的類型,實現終結器是沒有任何做用的。因此說,若是沒有特殊的需求應該避免重寫Finalize方法。

IDisposable接口

當垃圾回收生效時,能夠利用終結器來釋放非託管資源。然而,不少非託管資源都很是寶貴(如數據庫和文件句柄),因此它們應該儘量快的被清除,而不能依靠垃圾回收的發生。除了重寫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的調用。

重用using關鍵字

在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

Dispose和Finalize的結合

從前面的介紹瞭解到,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變量的存在,資源只會被釋放一次,多餘的調用會被忽略。

因此這個模式的優勢能夠總結爲:

  1. 若是沒有顯示的調用Dispose(),未釋放託管和非託管資源,那麼在垃圾回收時,還會執行Finalize(),釋放非託管資源,同時GC會釋放託管資源
  2. 若是調用了Dispose(),就能及時釋放了託管和非託管資源,那麼該對象被垃圾回收時,就不會執行Finalize(),提升了非託管資源的使用效率並提高了系統性能

總結

本文介紹了.NET垃圾回收中兩個相關的方法:Dispose和Finalize。Finalize的目的是用於釋放非託管的資源,而Dispose是用於釋放全部資源,包括託管的和非託管的。

Dispose須要在代碼中進行顯示的調用,而Finalize則是由垃圾回收自動調用,爲了更有效的結合Dispose和Finalize,文中還介紹了MSDN中給出的實現IDisposable接口的一個模式。

相關文章
相關標籤/搜索