天天看點

淺談C#托管程式中的資源釋放問題

便于對文章的開展,需要先明确兩個概念。

第一個就是很多人用.Net寫程式,會談到托管這個概念。那麼.Net所指的資源托管到底是什麼意思,是相對于所有資源,還是隻限于某一方面資源?很多人對此不是很了解,其實.Net所指的托管隻是針對記憶體這一個方面,并不是對于所有的資源;是以對于Stream,資料庫的連接配接,GDI+的相關對象,還有Com對象等等,這些資源并不是受到.Net管理而統稱為非托管資源。而對于記憶體的釋放和回收,系統提供了GC-Garbage Collector,而至于其他資源則需要手動進行釋放。

那麼第二個概念就是什麼是垃圾,通過我以前的文章,會了解到.Net類型分為兩大類,一個就是值類型,另一個就是引用類型。前者是配置設定在棧上,并不需要GC回收;後者是配置設定在堆上,是以它的記憶體釋放和回收需要通過GC來完成。GC的全稱為“Garbage Collector”,顧名思義就是垃圾回收器,那麼隻有被稱為垃圾的對象才能被GC回收。也就是說,一個引用類型對象所占用的記憶體需要被GC回收,需要先成為垃圾。那麼.Net如何判定一個引用類型對象是垃圾呢,.Net的判斷很簡單,隻要判定此對象或者其包含的子對象沒有任何引用是有效的,那麼系統就認為它是垃圾。

明确了這兩個基本概念,接下來說說GC的運作方式以及其的功能。記憶體的釋放和回收需要伴随着程式的運作,是以系統為GC安排了獨立的線程。那麼GC的工作大緻是,查詢記憶體中對象是否成為垃圾,然後對垃圾進行釋放和回收。那麼對于GC對于記憶體回收采取了一定的優先算法進行輪循回收記憶體資源。其次,對于記憶體中的垃圾分為兩種,一種是需要調用對象的析構函數,另一種是不需要調用的。GC對于前者的回收需要通過兩步完成,第一步是調用對象的析構函數,第二步是回收記憶體,但是要注意這兩步不是在GC一次輪循完成,即需要兩次輪循;相對于後者,則隻是回收記憶體而已。

很明顯得知,對于某個具體的資源,無法确切知道,對象析構函數什麼時候被調用,以及GC什麼時候會去釋放和回收它所占用的記憶體。那麼對于從C、C++之類語言轉換過來的程式員來說,這裡需要轉變觀念。

那麼對于程式資源來說,我們應該做些什麼,以及如何去做,才能使程式效率最高,同時占用資源能盡快的釋放。前面也說了,資源分為兩種,托管的記憶體資源,這是不需要我們操心的,系統已經為我們進行管理了;那麼對于非托管的資源,這裡再重申一下,就是Stream,資料庫的連接配接,GDI+的相關對象,還有Com對象等等這些資源,需要我們手動去釋放。

如何去釋放,應該把這些操作放到哪裡比較好呢。.Net提供了三種方法,也是最常見的三種,大緻如下:

1.  析構函數;

2.  繼承IDisposable接口,實作Dispose方法;

3.  提供Close方法。

經過前面的介紹,可以知道析構函數隻能被GC來調用的,那麼無法确定它什麼時候被調用,是以用它作為資源的釋放并不是很合理,因為資源釋放不及時;但是為了防止資源洩漏,畢竟它會被GC調用,是以析構函數可以作為一個補救方法。而Close與Dispose這兩種方法的差別在于,調用完了對象的Close方法後,此對象有可能被重新進行使用;而Dispose方法來說,此對象所占有的資源需要被标記為無用了,也就是此對象被銷毀了,不能再被使用。例如,常見SqlConnection這個類,當調用完Close方法後,可以通過Open重新打開資料庫連接配接,當徹底不用這個對象了就可以調用Dispose方法來标記此對象無用,等待GC回收。明白了這兩種方法的意思後,大家在往自己的類中添加的接口時候,不要歪曲了這兩者意思。

接下來說說這三個函數的調用時機,我用幾個試驗結果來進行說明,可能會使大家的印象更深。

首先是這三種方法的實作,大緻如下:

    /// <summary>

    /// The class to show three disposal function

    /// </summary>

    public class DisposeClass:IDisposable

    {

        public void Close()

        {

            Debug.WriteLine( "Close called!" );

        }

        ~DisposeClass()

            Debug.WriteLine( "Destructor called!" );

        #region IDisposable Members

        public void Dispose()

            // TODO:  Add DisposeClass.Dispose implementation

            Debug.WriteLine( "Dispose called!" );

        #endregion

    }

對于Close來說不屬于真正意義上的釋放,除了注意它需要顯示被調用外,我在此對它不多說了。而對于析構函數而言,不是在對象離開作用域後立刻被執行,隻有在關閉程序或者調用GC.Collect方法的時候才被調用,參看如下的代碼運作結果。

        private void Create()

            DisposeClass myClass = new DisposeClass();

        private void CallGC()

            GC.Collect();

        // Show destructor

        Create();

        Debug.WriteLine( "After created!" );

        CallGC();

運作的結果為:

After created!

Destructor called!

顯然在出了Create函數外,myClass對象的析構函數沒有被立刻調用,而是等顯示調用GC.Collect才被調用。

對于Dispose來說,也需要顯示的調用,但是對于繼承了IDisposable的類型對象可以使用using這個關鍵字,這樣對象的Dispose方法在出了using範圍後會被自動調用。例如:

    using( DisposeClass myClass = new DisposeClass() )

        //other operation here

如上運作的結果如下:

Dispose called!

那麼對于如上DisposeClass類型的Dispose實作來說,事實上GC還需要調用對象的析構函數,按照前面的GC流程來說,GC對于需要調用析構函數的對象來說,至少經過兩個步驟,即首先調用對象的析構函數,其次回收記憶體。也就是說,按照上面所寫的Dispose函數,雖說被執行了,但是GC還是需要執行析構函數,那麼一個完整的Dispose函數,應該通過調用GC.SuppressFinalize(this )來告訴GC,讓它不用再調用對象的析構函數中。那麼改寫後的DisposeClass如下:

            GC.SuppressFinalize( this );

通過如下的代碼進行測試。

        private void Run()

            using( DisposeClass myClass = new DisposeClass() )

            {

                //other operation here

            }

        Run();

        Debug.WriteLine( "After Run!" );

運作的結果如下:

After Run!

顯然對象的析構函數沒有被調用。通過如上的實驗以及文字說明,大家會得到如下的一個對比表格。

 析構函數                                            Dispose方法                                      Close方法

 銷毀對象                                           銷毀對象                                               關閉對象資源

 不能被顯示調用,會被GC調用               需要顯示調用或者通過using語句              需要顯示調用

 不确定                                              确定,在顯示調用或者離開using程式塊      确定,在顯示調用時

那麼在定義一個類型的時候,是否一定要給出這三個函數地實作呢。

我的建議大緻如下。

1. 提供析構函數,避免資源未被釋放,主要是指非記憶體資源;

2. 對于Dispose和Close方法來說,需要看所定義的類型所使用的資源(參看前面所說),而決定是否去定義這兩個函數;

3. 在實作Dispose方法的時候,一定要加上“GC.SuppressFinalize( this )”語句,避免再讓GC調用對象的析構函數。

C#程式所使用的記憶體是受托管的,但不意味着濫用,好地程式設計習慣有利于提高代碼的品質以及程式的運作效率。

<a target="_blank" href="http://blog.csdn.net/Knight94/archive/2006/08/05/1023352.aspx">http://blog.csdn.net/Knight94/archive/2006/08/05/1023352.aspx</a>

補充:.NET的GC機制有這樣兩個問題:

首先,GC并不是能釋放所有的資源。它不能自動釋放非托管資源。

第二,GC并不是實時性的,這将會造成系統性能上的瓶頸和不确定性。

為了解決第一個問題,.NET提供了析構函數,在C#中是~ClassName的形式。如果某個類定義了析構函數,.NET會在第一次的GC中調用析構函數,第二次才真正進行資源釋放。這就允許了我們能夠做一些手動的資源管理操作,手動對非托管資源進行清理。但是如果沒有必要,定義析構函數就會對性能造成較大的影響。

僅僅依賴析構函數對非托管資源進行釋放是不夠的,這是由于第二個問題:GC并不是實時性的,這會造成系統性能上的瓶頸和不确定性。是以有了IDisposable接口,IDisposable接口定義了Dispose方法,這個方法用來供程式員顯式調用以釋放非托管資源。

通常我們應該這樣寫程式:

        public class SampleClass : System.IDisposable

                public void Dispose()

                //供程式員顯式調用的Dispose方法

                {

                        Dispose(true);

                        //調用帶參數的Dispose方法,釋放托管和非托管資源

                        System.GC.SuppressFinalize(this);

                        //手動調用了Dispose釋放資源,那麼析構函數就是不必要的了,這裡阻止GC調用析構函數

                }

                protected void Dispose(bool disposing) 

                //protected的Dispose方法,保證不會被外部調用。

                //傳入bool值disposing以确定是否釋放托管資源

                        if (disposing)

                        {

                                //在這裡加入清理"托管資源"的代碼,應該是xxx.Dispose();

                        }

                        // 在這裡加入清理"非托管資源"的代碼

                ~SampleClass()

                //供GC調用的析構函數

                        Dispose(false);

                        //釋放非托管資源

        這樣一來,我們就像Delphi裡調用Object.Free方法一樣自然的調用Object.Dispose方法,而即使我們忘記了在合适的時候調用Dispose,GC也會在釋放對象的時候幫我們清理非托管資源的。GC所充當的角色隻是一種保障手段,它應該充當這種角色,我們不能過分依賴它。

        實際上,在較大的子產品退出時我們還應該及時地手動調用GC.Collect進行垃圾回收。

     本文轉自My_King1 51CTO部落格,原文連結:http://blog.51cto.com/apprentice/1360732,如需轉載請自行聯系原作者