1. 程式人生 > >.NET垃圾回收:非託管資源

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

其實在C#開發中,大部分資源都可以通過.NET垃圾回收機制進行回收,只用當我們使用非託管資源(原始的作業系統檔案控制代碼,原始的非託管資料庫連線,非託管記憶體等等)的時候,我們才需要實現自己的資源清理程式碼。

.NET提供了兩種釋放非託管資源的方式,型別自己的Finalize方法和IDisposable介面的Dispose方法。

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

Finalize方法

在.NET的基類System.Object中,定義了名為Finalize()的虛方法,這個方法預設什麼都不做。

我們可以為自定義的型別重寫Finalize方法,在該方法中加入必要的非託管資源清理邏輯。當要從記憶體中刪除這個型別的物件時,垃圾回收器會呼叫物件的Finalize方法。所以,無論.NET進行一次自發的垃圾回收,還是我們通過GC.Collect()進行強制垃圾回收,Finalize方法總是會被呼叫。另外,當承載應用程式的AppDomain從記憶體中移除時,同樣會呼叫Finalize方法。

重寫Finalize方法

假設我們現在有一個使用非託管資源的型別,那麼我們就需要重寫Finalize方法來進行非託管資源的清理,但是當通過下面的方式重寫Finalize方法的時候,我們會得到一個編譯錯誤。

C#

class MyResourceWrapper { protected override void Finalize() { } }

1

2

3

4

5

6

7

class MyResourceWrapper

{

    protected override void Finalize()

    {

 

    }

}

其實,當我們想要重寫Finalize方法時,C#為我們提供了(類似C++)解構函式語法(C#終結器)來重寫該方法。C#終結器和建構函式語法類似,方法名稱都和型別名稱一樣;不同的是,終結器具有~字首,並且不能使用訪問修飾符,不接受引數,也不能過載,所以一個類只能有一個終結器。

C#

class MyResourceWrapper { ~MyResourceWrapper() { Console.WriteLine("release unmanaged resources"); Console.Beep(); } }

1

2

3

4

5

6

7

8

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

.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被移除記憶體,型別終結器將被呼叫。

C#

static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); }

1

2

3

4

static void Main(string[] args)

{

    MyResourceWrapper mr = new MyResourceWrapper();

}

Finalize的工作機制

Finalize的工作機制還是比較複雜的,這裡只是簡單的介紹,更多的原理大家可以自己網上查查。

當在託管堆上分配物件空間時,執行庫會自動確定該物件是否提供一個自定義的Finalize方法。如果是這樣,物件被標記為可終結的,同時一個指向這個物件的指標被儲存在名為終結佇列的內部佇列中。終結佇列是一個由垃圾回收器維護的表,它指向每一個在從堆上刪除之前必須終結的物件。

當垃圾回收器確定到了從記憶體中釋放一個物件的時間時,它檢查終結佇列上的每一個項,並將物件從堆上覆制到另一個稱作終結可達表(finalization reachable table的託管結構上。此時,下一個垃圾回收時將產生另外一個執行緒,為每一個在可達表中的物件呼叫Finalize方法。因此,為了真正終結一個物件,至少要進行兩次垃圾回收。

從上面可以看到,Finalize方法的呼叫是相當消耗資源的。Finalize方法的作用是保證.NET物件能夠在垃圾回收時清理非託管資源,如果建立了一個不使用非託管資源的型別,實現終結器是沒有任何作用的。所以說,如果沒有特殊的需求應該避免重寫Finalize方法。

IDisposable介面

當垃圾回收生效時,可以利用終結器來釋放非託管資源。然而,很多非託管資源都非常寶貴(如資料庫和檔案控制代碼),所以它們應該儘可能快的被清除,而不能依靠垃圾回收的發生。除了重寫Finalize之外,類還可以實現IDisposable介面,然後在程式碼中主動呼叫Dispose方法來釋放資源。

看一個例子:

C#

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(); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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方法可以被呼叫到:

C#

static void Main(string[] args) { MyResourceWrapper mr = new MyResourceWrapper(); try { //do something wiht mr object } finally { mr.Dispose(); } }

1

2

3

4

5

6

7

8

9

10

11

12

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語句會導致一個編譯錯誤。

C#

static void Main(string[] args) { using (MyResourceWrapper mr = new MyResourceWrapper()) { //do something with mr object } }

1

2

3

4

5

6

7

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程式碼:

C#

.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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

.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,例如下面的程式碼:

C#

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); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

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介面的一個模式。