|
系列文章導(dǎo)航:
.NET Discovery 系列之一--string從入門到精通(上)
.NET Discovery 系列之二--string從入門到精通(勘誤版下)
.NET Discovery 系列之三--深入理解.NET垃圾收集機(jī)制(上)
.NET Discovery 系列之四--深入理解.NET垃圾收集機(jī)制(下)
.NET Discovery 系列之五--Me JIT(上)
.NET Discovery 系列之六--Me JIT(下)
.NET Discovery 系列之七--深入理解.NET垃圾收集機(jī)制(拾貝篇)
前言:
組成.NET平臺一個很重要的部分----垃圾收集器(Garbage Collection),今天我們就來講講它。想想看沒有GC,.NET還能稱之為一個平臺嗎?各種語言雖然都被編譯成MSIL,但是運行時的資源回收工作卻“各自為戰(zhàn)”,這樣不但增加了編程難度,也會使內(nèi)存管理工作變得復(fù)雜無比(不同語言處理內(nèi)存的微小差異,將在回收資源時被放大),更也不利于平臺移植。
這篇文章將全面的為大家介紹.NET 垃圾收集的運行方式、算法,以及與垃圾收集相關(guān)的關(guān)鍵方法。
說到垃圾收集機(jī)制,很少有人知道,垃圾收集并不是伴隨Java出現(xiàn)的,早在1958年,圖林獎得主John發(fā)明的Lisp語言就已經(jīng)提供了GC的功能,這是GC的第一次出現(xiàn),是思想的一次閃光!而后,1984年Dave Ungar發(fā)明的Small talk語言第一次正式采用了GC機(jī)制。
.NET的垃圾回收機(jī)制是個很大的話題,如果你沒接觸過類似C++那樣的語言,就很難理解GC是一個多么重要、令人興奮的東西:
1.提高軟件系統(tǒng)的內(nèi)聚。
2.降低編程復(fù)雜度,使程序員不必分散精力去處理析構(gòu)。
3.不妨礙設(shè)計師進(jìn)行系統(tǒng)抽象。
4.減少由于內(nèi)存運用不當(dāng)產(chǎn)生的Bug。
正文:
本文將通過“GC的算法與工作方式”、“GC關(guān)鍵方法解析”兩節(jié)來講述.NET垃圾收集機(jī)制。
第一節(jié). GC的算法與工作方式
1.算法
垃圾收集器的本質(zhì),就是跟蹤所有被引用到的對象,整理對象不再被引用的對象,回收相應(yīng)的內(nèi)存。
這聽起來類似于一種叫做“引用計數(shù)(Reference Counting)”的算法,然而這種算法需要遍歷所有對象,并維護(hù)它們的引用情況,所以效率較低些,并且在出現(xiàn)“環(huán)引用”時很容易造成內(nèi)存泄露。所以.NET中采用了一種叫做“標(biāo)記與清除(Mark Sweep)”算法來完成上述任務(wù)。
“標(biāo)記與清除”算法,顧名思義,這種算法有兩個本領(lǐng):
“標(biāo)記”本領(lǐng)——垃圾的識別:從應(yīng)用程序的root出發(fā),利用相互引用關(guān)系,遍歷其在Heap上動態(tài)分配的所有對象,沒有被引用的對象不被標(biāo)記,即成為垃圾;存活的對象被標(biāo)記,即維護(hù)成了一張“根-對象可達(dá)圖”。
其實,CLR會把對象關(guān)系看做“樹圖”,無疑,了解數(shù)據(jù)結(jié)構(gòu)的同學(xué)都知道,有了“樹圖”的概念,會加快遍歷對象的速度。
檢測、標(biāo)記對象引用,是一件很有意思的事情,有很多方法可以做到,但是只有一種是效率最優(yōu)的,.NET中是利用棧來完成的,在不斷的入棧與出棧中完成檢測:先在樹圖中選擇一個需要檢測的對象,將該對象的所有引用壓棧,如此反復(fù)直到棧變空為止。棧變空意味著已經(jīng)遍歷了這個局部根(或者說是樹圖中的節(jié)點)能夠到達(dá)的所有對象。樹圖節(jié)點范圍包括局部變量(實際上局部變量會很快被回收,因為它的作用域很明顯、很好控制)、寄存器、靜態(tài)變量,這些元素都要重復(fù)這個操作。一旦完成,便逐個對象地檢查內(nèi)存,沒有標(biāo)記的對象變成了垃圾。
“清除”本領(lǐng)——回收內(nèi)存:啟用Compact算法,對內(nèi)存中存活的對象進(jìn)行移動,修改它們的指針,使之在內(nèi)存中連續(xù),這樣空閑的內(nèi)存也就連續(xù)了,這就解決了內(nèi)存碎片問題,當(dāng)再次為新對象分配內(nèi)存時,CLR不必在充滿碎片的內(nèi)存中尋找適合新對象的內(nèi)存空間,所以分配速度會大大提高。但是大對象(large object heap)除外,GC不會移動一個內(nèi)存中巨無霸,因為它知道現(xiàn)在的CPU不便宜。通常,大對象具有很長的生存期,當(dāng)一個大對象在.NET托管堆中產(chǎn)生時,它被分配在堆的一個特殊部分中,移動大對象所帶來的開銷超過了整理這部分堆所能提高的性能。
Compact算法除了會提高再次分配內(nèi)存的速度,如果新分配的對象在堆中位置很緊湊的話,高速緩存的性能將會得到提高,因為一起分配的對象經(jīng)常被一起使用(程序的局部性原理),所以為程序提供一段連續(xù)空白的內(nèi)存空間是很重要的。
2.代齡(Generation)
代齡就是對Heap中的對象按照存在時間長短進(jìn)行分代,最短的分在第0代,最長的分在第2代,第2代中的對象往往是比較大的。Generation的層級與FrameWork版本有關(guān),可以通過調(diào)用GC.MaxGeneration得知。
通常,GC會優(yōu)先收集那些最近分配的對象(第0代),這與操作系統(tǒng)經(jīng)典內(nèi)存換頁算法“最近最少使用”算法如出一轍。但是,這并不代表GC只收集最近分配的對象,通常,.NET GC將堆空間按對象的生存期長短分成3代:新分配的對象在第0代(0代空間最大長度通常為256K),按地址順序分配,它們通常是一些局部變量;第1代(1代空間最大長度通常為2 MB)是經(jīng)過0代垃圾收集后仍然駐留在內(nèi)存中的對象,它們通常是一些如表單,按鈕等對象;第2代是經(jīng)歷過幾次垃圾收集后仍然駐留在內(nèi)存中的對象,它們通常是一些應(yīng)用程序?qū)ο蟆?/p>
當(dāng)內(nèi)存吃緊時(例如0代對象充滿),GC便被調(diào)入執(zhí)行引擎——也就是CLR——開始對第0代的空間進(jìn)行標(biāo)記與壓縮工作、回收工作,這通常小于1毫秒。如果回收后內(nèi)存依然吃緊,那么GC會繼續(xù)回收第1代(回收操作通常小于10毫秒)、第2代,當(dāng)然GC有時并不是按照第0、1、2代的順序收集垃圾的,這取決于運行時的情況,或是手動調(diào)用GC.Collect(i)指定回收的代。當(dāng)對第2代回收后任然無法獲得足夠的內(nèi)存,那么系統(tǒng)就會拋出OutOfMemoryException異常。
當(dāng)經(jīng)過幾次GC過后,0代中的某個對象仍然存在,那么它將被移動到第1代。同理,第1、2代也按同樣的邏輯運行。
這里還要說的是,GC Heap中代的數(shù)量與容量,都是可變的(這由一個“策略引擎”控制,在第二節(jié)中,會介紹到“策略引擎”), 以下代碼結(jié)合Windbg可以說明這個問題,以下代碼中,可以通過單擊按鈕“button1”,不斷的分配內(nèi)存,而后獲得對象“a”的代齡情況,并且在Form加載時也會獲得“a”的代齡。
public partial class Form1 : Form
{
private string a = new string('a',1);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
a = new string('a', 900000);
label1.Text = GC.GetGeneration(a).ToString();
}
private void Form1_Load(object sender, EventArgs e)
{
label1.Text = GC.GetGeneration(a).ToString();
}
}
NET技術(shù):.NET Discovery 系列之三--深入理解.NET垃圾收集機(jī)制(上),轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。