在開發.NET程序過程當中,因爲CLR中的垃圾回收(garbage collection)機制會管理已分配的對象,因此程序員就能夠不用關注對象何時釋放內存空間了。可是,瞭解垃圾回收機制仍是頗有必要的,下面咱們就看看.NET垃圾回收機制的相關內容。程序員
在C#中,咱們能夠經過new關鍵字建立一個引用類型的對象,好比下面一條語句。New關鍵字建立了一個Student類型的對象,這個新建的對象會被存放在託管堆中,而這個對象的引用會存放在調用棧中。(對於引用類型能夠查看,C#中值類型和引用類型)編程
Student s1 = new Student();
在C#中,當上面的Student對象被建立後,程序員就能夠不用關心這個對象何時被銷燬了,垃圾回收器將會在該對象再也不須要時將其銷燬。安全
當一個進程初始化後,CLR就保留一塊連續的內存空間,這段連續的內存空間就是咱們說的託管堆。.NET垃圾回收器會管理並清理託管堆,它會在必要的時候壓縮空的內存塊來實現優化,爲了輔助垃圾回收器的這一行爲,託管堆保存着一個指針,這個指針準確地只是下一個對象將被分配的位置,被稱爲下一個對象的指針(NextObjPtr)。爲了下面介紹垃圾回收機制,咱們先詳細看看new關鍵字都作了什麼。app
當C#編譯器遇到new關鍵字時,它會在方法的實現中加入一條CIL newobj命令,下面是經過ILSpy看到的IL代碼。函數
IL_0001: newobj instance void GCTest.Student::.ctor()
其實,newobj指令就是告訴CLR去執行下列操做:性能
按照上面的分析,當咱們建立兩個Student對象的時候,託管堆就應該跟下圖一致,NextObjPtr指向託管堆新的可用地址。優化
託管堆的大小不是無限制的,若是咱們一直使用new關鍵字來建立新的對象,託管堆就可能被耗盡,這時託管堆能夠檢測到NextObjPtr指向的空間超過了託管堆的地址空間,就須要作一次垃圾回收了,垃圾回收器會從託管堆中刪除不可訪問的對象spa
垃圾回收器是如何肯定一個對象再也不須要,能夠被安全的銷燬?線程
這裏就要看一個應用程序根(application root)的概念。根(root)就是一個存儲位置其中保存着對託管堆上一個對象的引用,根能夠屬性下面任何一個類別:指針
垃圾回收能夠分爲兩個步驟:
下面結合應用程序的根的概念,咱們來看看垃圾回收這兩個步驟。
在垃圾回收的過程當中,垃圾回收器會認爲託管堆中的全部對象都是垃圾,而後垃圾回收器會檢查全部的根。爲此,CLR會創建一個對象圖,表明託管堆上全部可達對象。
假設託管堆中有A-G七個對象,垃圾回收過程當中垃圾回收器會檢查全部的對象是否有活動根。這個例子的垃圾回收過程能夠描述以下(灰色表示不可達對象):
代碼中頗有可能多個對象中引用了同一個對象E,垃圾回收器只要檢測到對象E已經被標記過,則再也不對對象E內所引用的對象進行檢測,這樣作有兩個目的:一是提升性能,二是避免無限循環。
全部的根對象都檢查完以後,有標記的對象就是可達對象,未標記的對象就是不可達對象。
繼續上面的例子,垃圾回收器將銷燬全部未被標記的對象,釋放這些垃圾對象所佔的內存,再把可達對象移動到這裏以壓縮堆。
注意,在移動可達對象以後,全部引用這些對象的變量將無效,接着垃圾回收器要從新遍歷應用程序的全部根來修改它們的引用。在這個過程當中若是各個線程正在執行,極可能致使變量引用到無效的對象地址,因此整個進程的正在執行託管代碼的線程是被掛起的。
通過了垃圾回收以後,全部的非垃圾對象被移動到一塊兒,而且全部的非垃圾對象的指針也被修改爲移動後的內存地址,NextObjPtr指向最後一個非垃圾對象的後面。
當CLR試圖尋找不可達對象的時候,它須要遍歷託管堆上的對象。隨着程序的持續運行,託管堆可能愈來愈大,若是要對整個託管堆進行垃圾回收,勢必會嚴重影響性能。因此,爲了優化這個過程,CLR中使用了"代"的概念,託管堆上的每個對象都被指定屬於某個"代"(generation)。
"代"這個概念的基本思想就是,一個對象在託管堆上存在的時間越長,那麼它就更可能應該保留。託管堆中的對象能夠被分爲0、一、2三個代:
下面仍是經過一個例子看看代這個概念(灰色表明不可達對象):
經過前面的描述能夠看到,分代能夠避免每次垃圾回收都遍歷整個託管堆,這樣能夠提升垃圾回收的性能。
.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()強制垃圾回收:
在使用強制垃圾回收時,建議同時調用"GC.WaitForPendingFinalizers();",這樣能夠肯定在程序繼續執行以前,全部的可終結對象都必須執行必要的清除工做。可是要注意,GC.WaitForPendingFinalizers()會在回收過程當中掛起調用的線程。
static void Main(string[] args) { …… GC.Collect(); GC.WaitForPendingFinalizers(); …… }
每一次垃圾回收過程都會損耗性能,因此要儘可能避免經過GC.Collect()進行強制垃圾回收,除非遇到了真的須要強制垃圾回收的狀況。
本文介紹了.NET垃圾回收機制的基本工做過程,垃圾回收器經過遍歷託管堆上的對象進行標記,而後清除全部的不可達對象;在託管堆上的對象都被設置了一個代,經過了代這個概念,垃圾回收的性能獲得了優化。
下一篇咱們看看可終結對象(Finalize)和可處置對象(IDisposable)。