.NET垃圾回收 – 原理淺析

在開發.NET程序過程當中,因爲CLR中的垃圾回收(garbage collection)機制會管理已分配的對象,因此程序員就能夠不用關注對象何時釋放內存空間了。可是,瞭解垃圾回收機制仍是頗有必要的,下面咱們就看看.NET垃圾回收機制的相關內容。程序員

建立對象

在C#中,咱們能夠經過new關鍵字建立一個引用類型的對象,好比下面一條語句。New關鍵字建立了一個Student類型的對象,這個新建的對象會被存放在託管堆中,而這個對象的引用會存放在調用棧中。(對於引用類型能夠查看,C#中值類型和引用類型)編程

Student s1 = new Student();

在C#中,當上面的Student對象被建立後,程序員就能夠不用關心這個對象何時被銷燬了,垃圾回收器將會在該對象再也不須要時將其銷燬。安全

當一個進程初始化後,CLR就保留一塊連續的內存空間,這段連續的內存空間就是咱們說的託管堆。.NET垃圾回收器會管理並清理託管堆,它會在必要的時候壓縮空的內存塊來實現優化,爲了輔助垃圾回收器的這一行爲,託管堆保存着一個指針,這個指針準確地只是下一個對象將被分配的位置,被稱爲下一個對象的指針(NextObjPtr)。爲了下面介紹垃圾回收機制,咱們先詳細看看new關鍵字都作了什麼。app

new關鍵字

當C#編譯器遇到new關鍵字時,它會在方法的實現中加入一條CIL newobj命令,下面是經過ILSpy看到的IL代碼。函數

IL_0001: newobj instance void GCTest.Student::.ctor()

其實,newobj指令就是告訴CLR去執行下列操做:性能

  • 計算新建對象所須要的內存總數
  • 檢查託管堆,確保有足夠的空間來存放新建的對象
    • 若是空間足夠,調用類型的構造函數,將對象存放在NextObjPtr指向的內存地址
    • 若是空間不夠,就會執行一次垃圾回收來清理託管堆(若是空間依然不夠,就會報出OutofMemoryException)
  • 最後,移動NextObjPtr指向託管堆下一個可用地址,而後將對象引用返回給調用者

按照上面的分析,當咱們建立兩個Student對象的時候,託管堆就應該跟下圖一致,NextObjPtr指向託管堆新的可用地址。優化

託管堆的大小不是無限制的,若是咱們一直使用new關鍵字來建立新的對象,託管堆就可能被耗盡,這時託管堆能夠檢測到NextObjPtr指向的空間超過了託管堆的地址空間,就須要作一次垃圾回收了,垃圾回收器會從託管堆中刪除不可訪問的對象spa

應用程序的根

垃圾回收器是如何肯定一個對象再也不須要,能夠被安全的銷燬?線程

這裏就要看一個應用程序根(application root)的概念。根(root)就是一個存儲位置其中保存着對託管堆上一個對象的引用,根能夠屬性下面任何一個類別:指針

  • 全局對象和靜態對象的引用
  • 應用程序代碼庫中局部對象的引用
  • 傳遞進一個方法的對象參數的引用
  • 等待被終結(finalize,後面介紹)對象的引用
  • 任何引用對象的CPU寄存器

垃圾回收能夠分爲兩個步驟:

  1. 標記對象
  2. 壓縮託管堆

下面結合應用程序的根的概念,咱們來看看垃圾回收這兩個步驟。

標記對象

在垃圾回收的過程當中,垃圾回收器會認爲託管堆中的全部對象都是垃圾,而後垃圾回收器會檢查全部的根。爲此,CLR會創建一個對象圖,表明託管堆上全部可達對象。

假設託管堆中有A-G七個對象,垃圾回收過程當中垃圾回收器會檢查全部的對象是否有活動根。這個例子的垃圾回收過程能夠描述以下(灰色表示不可達對象):

  1. 當發現有根引用了託管堆中的對象A時,垃圾回收器會對此對象A進行標記
  2. 對一個根檢測完畢後會接着檢測下一個根,執行步驟一種一樣的標記過程,標記對象B,在標記B時,檢測到對象B內又引用了另外一個對象E,則也對E進行標記;因爲E引用了G,一樣的方式G也會被標記
  3. 重複步驟二,檢測Globales根,此次標記對象D

代碼中頗有可能多個對象中引用了同一個對象E,垃圾回收器只要檢測到對象E已經被標記過,則再也不對對象E內所引用的對象進行檢測,這樣作有兩個目的:一是提升性能,二是避免無限循環

全部的根對象都檢查完以後,有標記的對象就是可達對象,未標記的對象就是不可達對象。

壓縮託管堆

繼續上面的例子,垃圾回收器將銷燬全部未被標記的對象,釋放這些垃圾對象所佔的內存,再把可達對象移動到這裏以壓縮堆。

注意,在移動可達對象以後,全部引用這些對象的變量將無效,接着垃圾回收器要從新遍歷應用程序的全部根來修改它們的引用。在這個過程當中若是各個線程正在執行,極可能致使變量引用到無效的對象地址,因此整個進程的正在執行託管代碼的線程是被掛起的。

通過了垃圾回收以後,全部的非垃圾對象被移動到一塊兒,而且全部的非垃圾對象的指針也被修改爲移動後的內存地址,NextObjPtr指向最後一個非垃圾對象的後面。

對象的代

當CLR試圖尋找不可達對象的時候,它須要遍歷託管堆上的對象。隨着程序的持續運行,託管堆可能愈來愈大,若是要對整個託管堆進行垃圾回收,勢必會嚴重影響性能。因此,爲了優化這個過程,CLR中使用了"代"的概念,託管堆上的每個對象都被指定屬於某個"代"(generation)。

"代"這個概念的基本思想就是,一個對象在託管堆上存在的時間越長,那麼它就更可能應該保留。託管堆中的對象能夠被分爲0、一、2三個代:

  • 0代:從沒有被標記爲回收的新分配的對象
  • 1代:在上一次垃圾回收中沒有被回收的對象
  • 2代:在一次以上的垃圾回收後仍然沒有被回收的對象

下面仍是經過一個例子看看代這個概念(灰色表明不可達對象):

  1. 在程序初始化時,託管堆上沒有對象,這時候新添到託管堆上的對象是的代是0,這些對象歷來沒有通過垃圾回收器檢查。假設如今託管堆上有A-G七個對象,託管堆空間將要耗盡。

  2. 若是如今須要更多的託管堆空間來存放新建的對象(H、I、J),CLR就會觸發一次垃圾回收。垃圾回收器就會檢查全部的0代對象,全部的不可達對象都會被清理,全部沒有被回收掉的對象就成爲了1代對象。

  3. 假設如今須要更多的託管堆空間來存放新建的對象(K、L、M),CLR會再觸發一次垃圾回收。垃圾回收器會先檢查全部的0代對象,可是仍須要更多的空間,那麼垃圾回收器會繼續檢查全部 的1代對象,整理出足夠的空間。這時,沒有被回收的1代對象將成爲2代對象。2代對象是目前垃圾回收器的最高代,當再次垃圾回收時,沒有回收的對象的代數依然保持2。

經過前面的描述能夠看到,分代能夠避免每次垃圾回收都遍歷整個託管堆,這樣能夠提升垃圾回收的性能。

System.GC

.NET類庫中提供了System.GC類型,經過該類型的一些靜態方法,能夠經過編程的方式與垃圾回收器進行交互。

看一個簡單的例子:

class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Gender { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false));

        Console.WriteLine("This OS has {0} object generations", GC.MaxGeneration);

        Student s = new Student { Id = 1, Name = "Will", Age = 28, Gender = "Male"};
        Console.WriteLine(s.ToString());

        Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));

        GC.Collect();
        Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));

        GC.Collect();
        Console.WriteLine("Generation of s is: {0}", GC.GetGeneration(s));

        Console.Read();
    }
}

程序的輸出爲:

從這個輸出,咱們也能夠驗證代的概念,每次垃圾清理後,若是一個對象沒有被清理,那麼它的代就會提升。

強制垃圾回收

因爲託管堆上的對象由垃圾管理器幫咱們管理,全部咱們不須要關心託管堆上對象的銷燬以及內存空間的回收。

可是,有些特殊的狀況下,咱們可能須要經過GC.Collect()強制垃圾回收:

  1. 應用程序將要進入一段代碼,這段代碼不但願被可能的垃圾回收中斷
  2. 應用程序剛剛分配很是多的對象,程序想在使用完這些對象後儘快的回收內存空間

在使用強制垃圾回收時,建議同時調用"GC.WaitForPendingFinalizers();",這樣能夠肯定在程序繼續執行以前,全部的可終結對象都必須執行必要的清除工做。可是要注意,GC.WaitForPendingFinalizers()會在回收過程當中掛起調用的線程。

static void Main(string[] args)
{
    ……
    GC.Collect();
    GC.WaitForPendingFinalizers();
    ……
}

每一次垃圾回收過程都會損耗性能,因此要儘可能避免經過GC.Collect()進行強制垃圾回收,除非遇到了真的須要強制垃圾回收的狀況。

總結

本文介紹了.NET垃圾回收機制的基本工做過程,垃圾回收器經過遍歷託管堆上的對象進行標記,而後清除全部的不可達對象;在託管堆上的對象都被設置了一個代,經過了代這個概念,垃圾回收的性能獲得了優化。

下一篇咱們看看可終結對象(Finalize)和可處置對象(IDisposable)。

相關文章
相關標籤/搜索