.NET對象清理

敬告:本篇文章是我原創所寫,首發於 51CTO 技術網站,未經本人授權任何網站、公衆號、App 不允許轉載,授權的網站、公衆號、App 需明確標識本篇文章首發地址。需轉載請聯繫 [email protected]

在 .NET 中垃圾回收和資源清理是重中之重的內容,也是所有程序都必須用到的機制,但是有很大一部分開發人員並不知道垃圾回收和資源清理的原理。那麼,我將通過這篇文章向各位讀者詳細講解一下垃圾回收和資源清理。

一、垃圾回收

.NET中垃圾回收是運行時的核心功能,它的作用是回收不再被引用的對象所佔用的內存。這裏我們要注意垃圾回收器只回收內存資源而不處理其他資源。此外垃圾回收器是根據是否存在任何引用來決定要清理那些東西,也就是說垃圾回收器處理的是不被引用的引用對象,並且只能回收堆上的內存。

  1. 簡述
    在 .NET 中垃圾回收的很多細節都和 CLI 有關,我們常用的 Microsoft.NET 框架中實現垃圾回收的算法是 mark-and-compact 算法 。當每次一次垃圾回收週期開始時,它會查找對象的所有根引用,(一般來說根引用來自靜態變量、CPU寄存器和局部變量或參數實例的任何引用)。基於查找到的所有根引用,垃圾回收器就可以遍歷每個根引用標識的樹形結構,並遞歸確定每個根引用指向的對象,進而識別出所有可達對象。 當執行垃圾回收時,垃圾回收器會將所有可達對象一個挨一個的放在一起,這樣就可以覆蓋不可達對象所佔用的內存。爲了定位和移動可達對象,進程中所有託管線程都會在垃圾回收期間暫停運行,這樣就可以保證垃圾回收器在運行期間維持狀態一致性。雖然這麼做會造成應用程序短暫停止工作,但是一般來說只要垃圾回收週期不是特別長,這個短暫的停止工作是很難發覺的。在我們開發時有時可能不希望在運行一些代碼段時執行垃圾回收,這時我們可以在代碼段之前使用 System.GC 對象所包含的 Collect 方法來讓垃圾回收暫時跳過這些代碼。當然這麼做是不會阻止垃圾回收運行的,只是減少了這部分代碼可能被回收的概率,但是這裏有一個前提條件:代碼段執行期間不會發生內存被大量消耗使用的情況。
    在 .NET 中垃圾回收有一個特別的地方,就是並非所有的垃圾都會在一個垃圾回收週期內被回收。這是爲什麼呢?因爲在 .NET 垃圾回收器中有一個名字叫 generation 的概念,翻譯成中文就是 。它會清理那些生存時間較短的對象,那些在一次垃圾回收週期中存活下來的對象會降低清理頻率。也就是說當一個對象在一次垃圾回收週期中存活下來,那麼它將會被移動到下一代中,如果它又在一次垃圾回收週期中存活下來,那麼它將被移動到最後一代,也就是第二代(爲什麼是第二代呢?因爲 .NET 垃圾回收機制中代是從 0 開始的),第零代清理速度最快,第二代清理速度最慢。
  2. 弱引用
    弱引用這個名詞很少有開發人員聽過,所謂的弱引用是爲創建起來開銷很高並且維護成本也很大的對象而設計的。它不阻止垃圾回收器對對象的回收,但會維持一個引用,進而可以在被垃圾回收器回收之前可以重用。例如我們從數據庫中查詢一個龐大的數據列表向用戶展示,如果沒有使用弱引用當用戶關閉了這個列表,那麼垃圾回收器就有很大可能將它回收,那麼當用戶再次查看這個列表時,程序又需要從數據庫查詢並加載出來,這種操作成本是很高昂的。如果使用瞭如引用,每次請求列表時代碼首先檢查列表是否被清除,如果沒有被清除就直接將列表展示給用戶,如果被清除了就從數據庫查詢並展示給用戶,這就相當於對象在內存中進行了緩存。如果開發人員認爲對象應該進行弱引用,那麼就可以把這個對象賦值給 System.WeakReference 。下面我們來看一個弱認證的簡單例子:
    WeakReference Data;
    public FileStream Date()
    {
        FileStream fs= (FileStream)Data.Target;
        if(data!=null)
        {
            return data;
        }
        // more code
        Data.Target=data;
        return data;
    }
    
    上面的代碼是一個標準的創建弱引用的代碼,我們可以看到在代碼中對變量 data 進行了 null 判斷,我們可以通過這個判斷來檢查垃圾回收器是否將其回收。這裏還有一個關鍵代碼 FileStream fs= (FileStream)Data.Target; 這裏將弱引用賦值給了強引用,這樣可以避免在檢查 null 後和訪問數據前,發生垃圾回收器回收弱引用。

二、資源清理

在前面一小節開頭我們說過垃圾回收之回收內存中的對象,那麼如果我們需要回收其他資源呢,例如數據庫連接、句柄、外部設備。這時我們就需要用到資源清理。

  1. 終結器
    終結器是一個允許開發人員通過代碼來清理類資源的東西。終結器最大的特徵是它不能在代碼中顯式調用,只有垃圾回收器負責對對象的實例調用終結器,因此開發人員無法在編譯時確定終結器在何時執行,只能夠確定終結器時對象中最後一次被調用的地方。
    終結器的定義也很簡單,只需要在類名之前加一個 ~ 符號即可。

    class Demo
    {
        public Demo(string name)
        {
            //more code
        }
        
        ~Demo()
        {
            Close();
        }
        public void Close()
        {
            //more code
        }
        //more code
    }
    

    上述代碼我們就定義了一個簡單的終結器,我們定義終結器的時候需要注意以下四點:

    • 終結器是不允許傳遞任何參數的,也不能重載它;
    • 因爲它是被垃圾回收器所調用,因此給終結器加上訪問修飾符是毫無意義的;
    • 如果父類中存在終結器,那麼將會作爲子類終結器的一部分被自動調用;
    • 終結器必須顯示的釋放資源。

    因爲終結器是在自己的線程中執行的,因此如果終結器中存在一個未處理的異常就會很難診斷髮現,因爲造成異常的情況並不清晰透明。所以我們必須避免在終結器中引發異常。

  2. using
    雖然終結器可以幫助我們在忘記顯式調用必要清理代碼的時候執行清理,但是因爲終結器的運行存在不確定性,因此我們只能將它作爲備用機制。正常情況下我們可以使用 using
    C# 中的 IDisposable 接口的 Dispose 方法爲我們提供了實現細節。我們先來看一段代碼。

    class Demo
    {
        MyFileStream fs =new myFileStram();
        //more code
        fs.Dispose();
        //more code
    }
    class MyFileStream:IDisposable
    {
        public MyFileStream(string path)
        {
            //more code
        }
        //more code
        ~MyFileStream
        {
            Dispose(false);
        }
        public void Close()
        {
            Dispose();
        }
        public void Dispose()
        {
            Dispose(true);
            System.GC.SuppressFinalize();
        }
        public void Dispose(bool para)
        {
            // more code
        }
    }
    

    上述代碼中我們顯式調用了 MyFileStream 類的 Dispose 方法。 Dispose 方法主要用來清理已經用過的資源,但是這裏存在一個問題,當我們調用 Dispose 方法時有可能會發生異常,這時我們就無法正確調用 Dispose 方法了,爲了避免這個問題我們需要加入 try…finally 塊。但是我們無法保證開發人員每次都會寫 try…finally ,這時我們可以使用 C# 提供的 using 語句,我們將上面的調用代碼修改一下:

    class Demo
    {
        using(MyFileStream fs =new myFileStram())
        {
            //more code
        }
    }
    

    這段代碼最終生成的 CIL 代碼和使用 try…finally 塊生成的代碼完全一樣。

  3. 垃圾回收、終結和 IDisposable
    在上一小節的代碼中我們看到在 Dispose 方法中我們調用了 System.GC.SuppressFinalize(); ,它的作用是從終結隊列中移除 MyFileStream 實例。因爲所有清理都在Dispose 方法中完成了,而不是等着終結器執行。如果不調用 System.GC.SuppressFinalize() 方法實例將會一直在終結隊列中,只有當終結方法被調用之後才能在垃圾回收器中被回收,那麼這就造成了託管資源垃圾回收處理時間的延遲。 Dispose 方法中調用了 Dispose(bool para) 方法,在這個方法裏我們可以清理資源並阻止終結器。其次,我們定義了 Close 方法來調用 Dispose(bool para) 方法,這樣終結器就可以調用 Dispose(bool para) 方法來關閉釋放資源。針對前一小結的代碼需要有如下幾點注意:

    • 只針對開銷大,成本高的對象實現終結器;
    • 如果類存在終結器那麼就必須實現 IDisposable ;
    • 不要在終結器中拋出異常;
    • 在 Dispose 方法中必須調用 System.GC.SuppressFinalize ;
    • 保證 Dispose 可以被重用;
    • 保證 Dispose 方法的簡單性;
    • 不能在終結器中調用未被終結的其他對象;
    • 如果父類存在終結器,再重寫時必須調用父類終結器;
    • 調用 Dispose 方法之後,將對象設爲不可用。

    在某些特殊情況下垃圾回收的對象有可能會被無意中重新引用一個待終結的對象。這樣,被重新引用的對象就不再是不可訪問的,所以不能當作垃圾被回收掉。假如對象的終結方法已經運行,那麼除非顯式標記爲要進行終結,否則終結方法不一定會再次運行。

三、小結

這篇文章詳細講解了垃圾回收和資源清理相關的知識,對於部分開發人員來說這部分知識可能晦澀難懂,但是隻要在實際項目中上手使用,我相信就可以很快的掌握和理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章