1. 程式人生 > >淺談.NET垃圾回收-Garbage Collector

淺談.NET垃圾回收-Garbage Collector

什麼是GC

GC(Garbage Collector),垃圾記憶體收集,它以應用程式的root為基礎,遍歷應用程式在Heap上動態分配的所有物件,通過識別它們是否被引用來確定哪些物件是已經死亡的、哪些仍需要被使用。已經不再被應用程式的root或者別的物件所引用的物件就是已經死亡的物件,即所謂的垃圾,需要被回收,也就是釋放或者銷燬物件所佔用的記憶體。

實現GC有多種演算法。比較常見的演算法有Reference Counting,Mark Sweep,Copy Collection等。目前主流的虛擬系統.NET CLR,Java VM和Rotor都是採用的Mark Sweep演算法。

.NET的GC機制只能自動回收託管資源,對於非託管資源不能自動回收:如影象物件,資料庫連線,檔案控制代碼,網路連線,COM封裝器物件(COM元件)等等

。在.net的類中,主要有以下各類:OleDBDataReader,StreamWriter,ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,mage,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等。

此外,由於GC演算法設計自身的特點,GC清理物件和釋放垃圾記憶體並不是實時的,程式人員並不知道具體的時間,因此可能會造成本身不再使用的物件記憶體並沒有釋放,而程式在其它地方在此對這些非託管資源進行了訪問,此時就有可能發生錯誤。並且這種錯誤是隨機的,有時候GC正好回收的物件的記憶體,程式執行正常,有時候GC還沒有來得及釋放這段記憶體,程式就會執行異常,從而造成系統執行的不確定性。

對於非託管物件,.net為程式設計師提供了IDisposable介面,IDisposable介面定義了Dispose方法,這個方法用來供程式設計師顯式呼叫以釋放非託管資源,也可以使用using語句簡化資源物件的管理和操作。

GC中用到的幾個函式

來介紹一下GC中用到的幾個函式:
GC.SuppressFinalize(this);//請求公共語言執行時不要呼叫指定物件的終結器。
GC.GetTotalMemory(false);//檢索當前認為要分配的位元組數。一個引數,指示此方法是否可以等待較短間隔再返回,以便系統回收垃圾和終結物件。
GC.Collect();//強制對所有代進行即時垃圾回收。

GC執行機制

大家都知道GC是一個後臺執行緒,他會週期性的查詢物件,然後呼叫Finalize()方法去銷燬他,開發人員繼承IDispose介面,呼叫Dispose方法,銷燬了物件,而GC並不知道。GC依然會呼叫Finalize()方法,而在.NET中Object.Finalize()方法是無法過載的,所以我們可以使用解構函式來阻止重複的釋放。我們呼叫完Dispose方法後,還有呼叫GC.SuppressFinalize(this)方法來告訴GC,不要再呼叫這些物件的Finalize()方法了。

在垃圾回收時儘量避免使用finallize來回收資源,這樣會造成兩車垃圾回收,影響效率。

垃圾回收器使用名為“終止佇列”的內部結構跟蹤具有Finalize方法的物件。當應用程式建立具有Finalize方法的物件時,垃圾回收器都在終止佇列中放置一個指向該物件的項。託管堆中所有需要在垃圾回收器回收其記憶體之前呼叫它們的終止程式碼的物件都在終止佇列中含有項。(實現Finalize方法或解構函式對效能可能會有負面影響,因此應避免不必要地使用它們。用Finalize方法回收物件使用的記憶體需要至少兩次垃圾回收。當垃圾回收器執行回收時,它只回收沒有終結器的不可訪問物件的記憶體。這時,它不能回收具有終結器的不可訪問物件。它改為將這些物件的項從終止佇列中移除並將它們放置在標為準備終止的物件列表中。該列表中的項指向託管堆中準備被呼叫其終止程式碼的物件。垃圾回收器為此列表中的物件呼叫Finalize方法,然後,將這些項從列表中移除。後來的垃圾回收將確定終止的物件確實是垃圾,因為標為準備終止物件的列表中的項不再指向它們。在後來的垃圾回收中,實際上回收了物件的記憶體。

垃圾收集器收集垃圾物件的規則

GC執行垃圾收集是一個非常複雜的演算法,大概可以描述成這樣:
CLR按物件在記憶體中的存活的時間長短,來收集物件。時間最短的被分配到第0代,最長的被分配到第2代,一共就3代。

一般第0代的物件都是較小的物件,第2代的物件都是較大的物件,第0代物件GC收集時間最短(毫秒級別),第2代的物件GC收集時間最長。當程式需要記憶體時(或者程式空閒的時),GC會先收集第0代的物件,收集完之後發現釋放的記憶體仍然不夠用,GC就會去收集第1代,第2代物件。(一般情況是按這個順序收集的)。

如果GC執行過幾次之後記憶體空間依然不夠用,那麼就丟擲了OutOfMemoryException異常。GC執行幾次之後,第0代的物件仍然存在,那麼CLR會把這些物件移動到第1代,第1代的物件也是這樣。

如果GC發現上一次收集了很多物件,釋放了很大的記憶體,那麼它就會盡快執行第二次回收。如果它頻繁的回收,但釋放的記憶體不多,那麼它就會減慢回收的頻率。所以,儘量不要呼叫GC.Collect()這樣會破壞GC現有的執行策略。除非你對你的應用程式記憶體使用情況非常瞭解,你知道何時會產生大量的垃圾,那麼你可以手動干預垃圾收集器的工作。

關於GC.Collect(),MSDN上官方文件中有如下一段話:
垃圾回收GC類提供GC.Collect方法,您可以使用該方法讓應用程式在一定程度上直接控制垃圾回收器。通常情況下,您應該避免呼叫任何回收方法,讓垃圾回收器獨立執行。在大多數情況下,垃圾回收器在確定執行回收的最佳時機方面更有優勢。但是,在某些不常發生的情況下,強制回收可以提高應用程式的效能。當應用程式程式碼中某個確定的點上使用的記憶體量大量減少時,在這種情況下使用GC.Collect方法可能比較合適。例如,應用程式可能使用引用大量非託管資源的文件。當您的應用程式關閉該文件時,您完全知道已經不再需要文件曾使用的資源了。出於效能的原因,一次全部釋放這些資源很有意義。

在垃圾回收器執行回收之前,它會掛起當前正在執行的所有執行緒。如果不必要地多次呼叫GC.Collect,這可能會造成效能問題。您還應該注意不要將呼叫GC.Collect的程式碼放置在程式中使用者可以經常呼叫的點上。這可能會削弱垃圾回收器中優化引擎的作用,而垃圾回收器可以確定執行垃圾回收的最佳時間。

測試和試驗

GC回收物件的機制和不確定性

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
    AA b=new AA();
    AA c=new AA();
    AA d=new AA();
}
public class AA{}

在講這個例子之前,要明白什麼被稱之為垃圾,垃圾就是一個記憶體區域,沒有被任何引用指向,或者不再會被用到。哪麼在第一次點選按鈕的時候會生成4個物件,第二次點選按鈕的時候也會生成4個物件,但是第一次生成的4個物件就已經是垃圾了,因為,第一次生成的4個物件隨著button1_Click函式的結束而不會再被呼叫(或者說不能再被呼叫),哪麼這個時候GC就會來回收嗎?不是的!我說了GC是隨機的,哪麼你只管點你的,不一會GC就會來回收的(這裡我們可以認為,記憶體中存在一定數量的垃圾之後,GC會來),要證明GC來過我們把AA類改成:

public class AA
{
    ~AA()
    {
        MessageBox.Show("解構函式被執行了");
    }
}

要明白,GC清理垃圾,實際上是呼叫解構函式,但是這些程式碼是託管程式碼(因為裡面沒有涉及到Steam,Connection等)所以在解構函式中,我們可以只寫一個MsgBox來證明剛的想法;這個時候,執行你的程式,一直點選按鈕,不一會就會出現一大堆的“解構函式被執行了”

好了,然後讓我們看看能不能改變GC這種為所欲為的天性,答案是可以的,我們可以通過呼叫GC.Collect();來強制GC進行垃圾回收,哪麼button1_Click修改如下:

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
    AA b=new AA();
    AA c=new AA();
    AA d=new AA();
    GC.Collect();
}

哪麼在點選第一次按鈕的時候,生成四個物件,然後強制垃圾回收,這個時候,會回收嗎?當然不會,因為,這四個物件還在執行中(方法還沒結束),當點第二次按鈕的時候,會出現四次”解構函式被執行了”,這是在釋放第一次點選按鈕的四個物件,然後以後每次點選都會出現四次”解構函式被執行了”,哪麼最後一次的物件什麼時候釋放的,在關閉程式的時候釋放(因為關閉程式要釋放所有的記憶體)。

回收非託管資源的方法

對於非託管程式碼,假設有如下一個類:

    public class AA
    {
        FileStream fs=new FileStream("D://a.txt",FileMode.Open);
        ~AA()
        {
            MessageBox.Show("解構函式被執行了");
        }
    }

private void button1_Click(object sender,EventArgs e)
{
    AA a=new AA();
}

點選第二次的時候就會報錯,原因是一個檔案只能建立一個連線。那麼一定要釋放掉第一個資源,才可以進行第二次的連線。如果採用GC.Collect(),來強制釋放閒置的資源,修改程式碼如下:

private void button1_Click(object sender,EventArgs e)
{
    GC.Collect();
    AA a=new AA();
}

可以看到,第二次點按鈕的時候,確實出現了“解構函式被執行了“,但是程式仍然錯了,原因前面我說過,因為Stream不是託管程式碼,所以C#不能幫我們回收,哪怎麼辦?自己寫一個Dispose方法;去釋放我們的記憶體。程式碼如下:

public class AA:IDisposable
{
    FileStream fs=new File Stream("D://a.txt",FileMode.Open);
    ~AA()
    {
    MessageBox.Show("解構函式被執行了");
    }
    #regionIDisposable成員
    public void Dispose()
    {
        fs.Dispose();
        MessageBox.Show("dispose執行了");
    }
    #endregion
}

繼承IDisposable介面以後會有一個Dispose方法(當然了,你不想繼承也可以,但是介面給我們提供一種規則,你不願意遵守這個規則,就永遠無法融入整個團隊,你的程式碼只有你一個人能看懂),好了閒話不說,這樣一來我們的button1_Click改為

private void button1_Click(objectsender,EventArgse)
{
    AA a=new AA();
    a.Dispose();
}

我們每次點選之後,都會發現執行了“dispose執行了”,在關閉程式的時候仍然執行了“解構函式被執行了”這意味了,GC還是工作了,哪麼如果程式改為:

private void button1_Click(objectsender,EventArgse)
{
    AAa=newAA();
    a.Dispose();
    GC.Collect();
}

每次都既有“dispose執行了又有”“解構函式被執行了”,這意味著GC又來搗亂了,哪麼像這樣包含Stream connection的物件,就不用GC來清理了,只需要我們加上最後一句話GC.SuppressFinalize(this)來告訴GC,讓它不用再呼叫物件的解構函式中。那麼改寫後的AA的dispose方法如下:

public void Dispose()
{
    fs.Dispose();
    MessageBox.Show("dispose執行了");
    GC.SuppressFinalize(this);
}

參考資料