.Net CLR垃圾收集原理(Understanding .net CLR garbage collection)

引言

內存管理是計算機科學中一個相當複雜而有趣的領域。在計算機誕生的這幾十年間,內存的管理的技術不斷進步,使系統能夠更加有效地利用內存這一計算機必不可少的資源。

一般而言,內存管理可以分爲三類:硬件管理(如TLB),操作系統管理(如Buddy System,Paging,Segmentation),應用程序管理(如C++,Java,.net的內存管理機制)。鑑於篇幅和筆者水平的限制,本文只涉及了內存管理的很小一部分,即.net中的內存管理方法。.net是一個當代的應用程序框架,採用了內存自動管理技術,就是通常所說的內存垃圾自動回收技術――Garbage Collection(下文中簡稱GC),對.net的剖析比較具有代表性。

GC的歷史與好處

雖然本文是以.net作爲目標來講述GC,但是GC的概念並非才誕生不久。早在1958年,由鼎鼎大名的圖林獎得主John McCarthy所實現的Lisp語言就已經提供了GC的功能,這是GC的第一次出現。Lisp的程序員認爲內存管理太重要了,所以不能由程序員自己來管理。但後來的日子裏Lisp卻沒有成氣候,採用內存手動管理的語言佔據了上風,以C爲代表。出於同樣的理由,不同的人卻又不同的看法,C程序員認爲內存管理太重要了,所以不能由系統來管理,並且譏笑Lisp程序慢如烏龜的運行速度。的確,在那個對每一個Byte都要精心計算的年代GC的速度和對系統資源的大量佔用使很多人的無法接受。而後,1984年由Dave Ungar開發的Small talk語言第一次採用了Generational garbage collection的技術(這個技術在下文中會談到),但是Small talk也沒有得到十分廣泛的應用。

直到20世紀90年代中期GC才以主角的身份登上了歷史的舞臺,這不得不歸功於Java的進步,今日的GC已非吳下阿蒙。Java採用VM(Virtual Machine)機制,由VM來管理程序的運行當然也包括對GC管理。90年代末期.net出現了,.net採用了和Java類似的方法由CLR(Common Language Runtime)來管理。這兩大陣營的出現將人們引入了以虛擬平臺爲基礎的開發時代,GC也在這個時候越來越得到大衆的關注。

爲什麼要使用GC呢?也可以說是爲什麼要使用內存自動管理?有下面的幾個原因:

l          提高了軟件開發的抽象度;

l          程序員可以將精力集中在實際的問題上而不用分心來管理內存的問題;

l          可以使模塊的接口更加的清晰,減小模塊間的偶合;

l          大大減少了內存人爲管理不當所帶來的Bug;

l          使內存管理更加高效。

總的說來就是GC可以使程序員可以從複雜的內存問題中擺脫出來,從而提高了軟件開發的速度、質量和安全性。

什麼是GC

GC如其名,就是垃圾收集,當然這裏僅就內存而言。Garbage Collector(垃圾收集器,在不至於混淆的情況下也成爲GC)以應用程序的root[1]爲基礎,遍歷應用程序在Heap上動態分配的所有對象[2],通過識別它們是否被引用來確定哪些對象是已經死亡的哪些仍需要被使用。已經不再被應用程序的root或者別的對象所引用的對象就是已經死亡的對象,即所謂的垃圾,需要被回收。這就是GC工作的原理。爲了實現這個原理,GC有多種算法。比較常見的算法有Reference Counting,Mark Sweep,Copy Collection等等。目前主流的虛擬系統.net CLR,Java VM和Rotor都是採用的Mark Sweep算法。本文以.net爲基礎,這裏只對Mark Sweep算法進行講述。

相關的GC算法

Mark Sweep

在程序運行的過程中,不斷的把Heap的分配空間給對象,當Heap的空間被佔用到不足以爲下一個對象分配的時候Mark Sweep算法被激活,將垃圾內存進行回收並將其返回到free list[3]中。

Mark Sweep就像它的名字一樣在運行的過程中分爲兩個階段,Mark階段和Sweep階段。Mark階段的任務是從root出發,利用相互的引用關係遍歷整個Heap,將被root和其它對象所引用的對象標記起來。沒有被標記的對象就是垃圾。之後是Sweep階段,這個階段的任務就是回收所有的垃圾。如圖1所示。

圖1:m就是被標記的對象

Mark Sweep算法雖然速度比Reference Counting要快,並且可以避免循環引用造成的內存泄漏。但是也有不少缺點,它需要遍歷Heap中所有的對象(存活的對象在Mark階段遍歷,死亡的對象在Sweep階段遍歷)所以速度也不是十分理想。而且對垃圾進行回收以後會造成大量的內存碎片。

爲了解決這兩個問題,Mark Sweep算法得到了改進。首先是在算法中加入了Compact階段,即先標記存活的對象,再移動這些對象使之在內存中連續,最後更新和對象相關的地址和free list。這就是Mark Compact算法,它解決了內存碎片的問題。而爲了提高速度,Generation的概念被引入了。

Generation

Generational garbage collector(又被稱爲ephemeral garbage collector)是基於以下幾個假設的:

l          對象越年輕則它的生命週期越短;

l          對象越老則它的生命週期越長;

l          年輕的對象和其它對象的關係比較強,被訪問的頻率也比較高;

l          對Heap一部分的回收壓縮比對整個Heap的回收壓縮要快。

Generation的概念就是對Heap中的對象進行分代(分成幾塊,每一塊中的對象生存期不同)管理。當對象剛被分配時位於Generation 0中,當Generation 0的空間將被耗盡時,Mark Compact算法被啓動。經過幾次GC後如果這個對象仍然存活則會將其移動到Generation 1中。同理,如果經過幾次GC後這對象還是存活的,則會被移動到Generation 2中,直到被移動到最高級中最後被回收或者是同程序一同死亡。 採用Generation的最大好處就在於每次GC不用對整個Heap都進行處理,而是每次處理一小塊。對於Generation 0中的對象,因爲它們死亡的可能性最大,所以對它們GC的次數可以安排多一些,而其它相對死亡的可能性小一些的對象所在的Generation可以少安排幾次GC。這樣做就使得GC的速度得到了一定程度的提高。這樣就產生了幾個有待討論的問題,首先是應該設置幾個Generation,每個Generation應該設置成多大,然後是對每個對象升級時它應該是已被GC了多少次而仍然存活。關於.net CLR對這個問題的處理,在本文的最後將給出一個例子對其進行測試。

相關的數據結構

與.net GC相關的數據結構有三個Managed Heap,Finalization Queue和Freachable Queue。

Managed Heap

Managed Heap是一個設計簡單而優化的堆,它與傳統的C-runtime的堆不太一樣。它的簡單管理方法是爲了提高對堆的管理速度,同時也是基於一個簡單的(也是不可能的)假設。對Managed Heap的管理假設內存是無窮無盡的。在Managed Heap上有一個稱爲NextObjPtr的指針,這個指針用於指示堆上最後一個對象的地址。當有一個新的對象要分配到這個堆上時,所要做的僅僅是將NextObjPtr的值加上新對象的大小形成新的NextObjPtr。這只是一個簡單的相加,當NextObjPtr的值超出了Managed Heap邊界的時候說明堆已經滿了,GC將被啓動。

圖2:相關數據結構示意圖

Finalization Queue和Freachable Queue

這兩個隊列和.net對象所提供的Finalize[4]方法有關。這兩個隊列並不用於存儲真正的對象,而是存儲一組指向對象的指針。當程序中使用了new操作符在Managed Heap上分配空間時,GC會對其進行分析,如果該對象含有Finalize方法則在Finalization Queue中添加一個指向該對象的指針。在GC被啓動以後,經過Mark階段分辨出哪些是垃圾。再在垃圾中搜索,如果發現垃圾中有被Finalization Queue中的指針所指向的對象,則將這個對象從垃圾中分離出來,並將指向它的指針移動到Freachable Queue中。這個過程被稱爲是對象的復生(Resurrection),本來死去的對象就這樣被救活了。爲什麼要救活它呢?因爲這個對象的Finalize方法還沒有被執行,所以不能讓它死去。Freachable Queue平時不做什麼事,但是一旦裏面被添加了指針之後,它就會去觸發所指對象的Finalize方法執行,之後將這個指針從隊列中剔除,這是對象就可以安靜的死去了。.net framework的System.GC類提供了控制Finalize的兩個方法,ReRegisterForFinalize和SuppressFinalize。前者是請求系統完成對象的Finalize方法,後者是請求系統不要完成對象的Finalize方法。ReRegisterForFinalize方法其實就是將指向對象的指針重新添加到Finalization Queue中。這就出現了一個很有趣的現象,因爲在Finalization Queue中的對象可以復生,如果在對象的Finalize方法中調用ReRegisterForFinalize方法,這樣就形成了一個在堆上永遠不會死去的對象,像鳳凰涅槃一樣每次死的時候都可以復生。

對GC的直接控制

.net framework的System.GC類提供一些可以對GC直接進行操作的方法。而System.Runtime.InteropServices.GCHandle類提供從非託管內存訪問託管對象的方法(這裏對此不作討論)。先來看下面的這個利用System.GC進行直接操作的例子。

using System;
 
namespace gcTest
{
     class gcDemo
     {
         private static void GenerationDemo()
         {
              // Let's see how many generations the GCH supports (we know it's 2)
              Console.WriteLine("Maximum GC generations: {0}", GC.MaxGeneration);
 
              // Create a new BaseObj in the heap
              GenObj obj = new GenObj("Generation");
 
              // Since this object is newly created, it should be in generation 0
              obj.DisplayGeneration();    // Displays 0
 
              for(int i = 1; i <= GC.MaxGeneration; i++)
              {
                   // Performing a garbage collection promotes the object's generation
                   GC.Collect();
                   obj.DisplayGeneration();    // Displays i
              }
 
              obj = null;         // Destroy the strong reference to this object
 
              for(int i = 0; i <= GC.MaxGeneration; i++)
              {
                   GC.Collect(i);                  
                   GC.WaitForPendingFinalizers();
                   //suspend this thread until the freachable queue of
                   //the i generation has been emptied
                   //only when i = GC.MaxGeneration, this finalization method
                   //of obj will be performed
              }
 
              Console.WriteLine("Demo stop: Understanding Generations.");
              //total gc times
              //generation 0 : 5 times
              //generation 1 : 4 times
              //generation 2 : 3 times
         }
 
         public static void Main()
         {
              GenerationDemo();
         }
     }
 
     class GenObj
     {
         private string objName;
 
         public GenObj(string name)
         {
              this.objName = name;
         }
 
         public void DisplayGeneration()
         {
              Console.WriteLine("I am in Generation {0}", GC.GetGeneration(this));
         }
     };
}

這是個有趣的例子,首先利用GC.MaxGeneration()得知了在.net CLR中的GC採用了3代的結構,即Generation 0~2。接下來在Managed Heap上分配了一個GenObj的實例obj。在開始時obj位於Generation 0中,然後對整個Managed Heap進行兩次GC。可以發現每進行一次GC存活的對象都會升一級直至到達Generation 2中。設置obj = null,這樣做是爲了取消root對obj的強引用,使obj成爲垃圾。緊接着利用GC.Collect(i)對Managed Heap逐級進行GC,這個方法會對Generation 0~i進行GC。GC.WaitForPendingFinalizers()的作用是使整個進程掛起,等到Freachable Queue中所指向的對象的Finalize方法被調用。這樣做的目的是爲了保障對本次GC所確定的垃圾進行完全的回收,而不會因爲對象的Finalize方法使對象復生。

這個例子得到的一些結果可以直觀的看出.net CLR對GC的處理,要想得到更具體的數據讀者可以使用Windows提供的性能監視器perfmon.exe對.net應用程序進行測試。

最後還要提到的是GC對大對象(large object)的處理,這個處理和以上所討論的大同小異,只是GC不會進行Compact這個過程,因爲要在內存中移動一個較大的對象對系統性能帶來的不良影響是顯而易見的。

結論

本文旨在讓讀者對.net CLR garbage collection有一個大致的理解,這裏作出的只是粗淺的討論,很多方面並沒有涉及,比如多線程狀態下GC的工作原理,各種不同版本的.net GC等等。有興趣深入下去朋友可以閱讀Rotor和mono的源碼,Microsoft .net framework的源碼是不可能得到的了。非常歡迎有興趣的朋友來與我討論。

參考資料

l          《Garbage Collection--問題和技術》裘宗燕

l          《.NET的自動內存管理》 蔡學庸

l          《Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework》 Jeffrey Richter

l          MSDN of Visual Studio .net

 

2003-8-23



[1] 能被程序所直接操作的值稱其爲root。

[2] Heap中一個獨立的數據塊。

[3] 用於記錄Heap中空閒空間的數據結構

[4] 關於finalize方法請查閱相關資料,本文不作介紹。


原文鏈接:http://blog.csdn.net/windfast_2000/article/details/14237

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