C#的 GC工作原理基礎(chǔ)
作為一位C++出身的C#程序員,我最初對垃圾收集(GC)抱有懷疑態(tài)度软吐,懷疑它是否能夠穩(wěn)定高效的運作瘩将;而到了現(xiàn)在,我自己不得不說我已經(jīng)逐漸習慣并依賴GC與我的程序“共同奔跑”了凹耙,對“delete”這個習慣于充當罪魁禍首的關(guān)鍵字也漸漸產(chǎn)生了陌生感姿现。然而實踐證明,我對GC的過分信賴卻招致了很多意想不到的錯誤肖抱,這也激勵了我對GC的運作機制作深入一步的了解备典。隨后我開始翻書,查資料意述,終于對GC有了一個比較完整的理解(但遠遠算不上深入)提佣。有人也許會說:“研究GC的內(nèi)部機制有什么價值嗎?我們是搞應(yīng)用程序開發(fā)的荤崇,客戶的機器可以達到很高的配置拌屏,內(nèi)存資源不是問題√焓裕”這種說法明顯是認為“垃圾收集=內(nèi)存釋放”了槐壳,其實在垃圾收集中,造成最多麻煩的往往不是內(nèi)存量喜每,而是在內(nèi)存釋放之外,GC暗地里為我們做的繁雜事務(wù)(例如非托管資源的清理和釋放)雳攘。如果你對GC的基本運作還不了解带兜,而又沒有時間仔細閱讀眾多技術(shù)資料的話,那么我的這幾篇文章或許對你能有一些幫助吨灭。
下面就從資源的分配和釋放入手刚照,先了解一下背景知識。
一. 托管資源的分配
CLR在運行時管理著一段內(nèi)存地址空間(虛擬地址空間喧兄,在運行中會映射到物理內(nèi)存地址中)无畔,分為“托管堆”和“棧”兩部分吠冤,棧用于存儲值類型數(shù)據(jù)浑彰,它會在方法執(zhí)行結(jié)束后自動銷毀其中引用的值類型變量,這一部分不屬于垃圾收集的范圍拯辙。托管堆用于引用類型的變量存儲郭变,是垃圾收集的關(guān)鍵陣地颜价。
托管堆是一段連續(xù)的地址空間,其中所分配出去的空間呈現(xiàn)出類似數(shù)組形態(tài)的隊列結(jié)構(gòu):
NextObjPtr是托管堆所維護的一個內(nèi)存指針诉濒,指示下一個對象分配的內(nèi)存起始地址周伦,它會隨著內(nèi)存的分配而不斷移動(當然也會隨著內(nèi)存垃圾回收而發(fā)生移動),永遠指向下一個空閑的地址未荒。
到了這里专挪,我們不妨與C++比較一下內(nèi)存分配機制的效率(對效率不感興趣的大可以跳過:)),順便讓C++的朋友們打消一些對CLR分配內(nèi)存效率的疑慮片排。在查找空閑內(nèi)存空間時寨腔,CLR只需要在NextObjPtr處直接留出指定大小的空間提供給數(shù)據(jù)初始化,然后計算新的空閑地址并重置NextObjPtr指針即可划纽。而在C/C++中脆侮,在分配內(nèi)存之前先要遍歷一遍內(nèi)存占用的鏈表以查找合適大小的內(nèi)存塊,然后再修改此鏈表勇劣,這樣也很容易產(chǎn)生內(nèi)存碎塊靖避,使得內(nèi)存分配性能下降。很明顯比默,.NET的分配方式效率更高幻捏。但是這種效率是以GC的勞動為代價的。
二. 垃圾判定
要進行垃圾收集命咐,首先要知道什么是垃圾篡九。GC通過遍歷應(yīng)用程序中的“根”來尋找垃圾。我們可以認為根是一個指向引用類型對象內(nèi)存地址的指針醋奠。如果一個對象沒有了根榛臼,就是它不再被任何位置所引用,那么它就是垃圾的候選者了窜司。
值得注意的一點是沛善,對象可能在其生存期結(jié)束之前就被列入垃圾名單,甚至已經(jīng)被GC所暗殺塞祈!那是因為對象可能在生存期的某一時刻已經(jīng)不再被引用金刁,如果在這個時候執(zhí)行垃圾收集,那么這個不幸的對象極有可能已經(jīng)被列為垃圾并被銷毀(為什么說是“可能”呢议薪?因為它不一定在GC的視力范圍內(nèi)尤蛮。后面講到“代齡”時會詳細介紹相關(guān)細節(jié))。
1publicstaticvoid Main()
2 {
3string sGarbage= "I'm here";
4
5//下面的代碼沒有再引用s斯议,它已經(jīng)成為垃圾對象---當然产捞,這樣的代碼本身也是垃圾;
6//此時如果執(zhí)行垃圾收集,則sGarbage可能已經(jīng)魂歸西天
7
8 Console.WriteLine("Main() is end");
9 }
三. 對象代齡
盡管GC總是在默默為我們勞動捅位,但它畢竟是由人創(chuàng)造的轧葛,人會偷懶搂抒,它也會。為了減少每次的工作量尿扯,它總是希望能夠減少工作的范圍求晶;它堅信,越晚創(chuàng)建的對象往往越短命衷笋,因此它會集中精力處理這一部分的內(nèi)存區(qū)域芳杏,暫且擱置其他部分。GC引入“代齡”的概念來劃分對象生存級別辟宗。
CLR初始化后的第一批被創(chuàng)建的對象被列為0代對象爵赵。CLR會為0代對象設(shè)定一個容量限制,當創(chuàng)建的對象大小超過這個設(shè)定的容量上限時泊脐,GC就會開始工作空幻,工作的范圍是0代對象所處的內(nèi)存區(qū)域,然后開始搜尋垃圾對象容客,并釋放內(nèi)存秕铛。當GC工作結(jié)束后,幸存的對象將被列為第1代對象而保留在第1代對象的區(qū)域內(nèi)缩挑。此后新創(chuàng)建的對象將被列為新的一批0代對象但两,直到0代的內(nèi)存區(qū)域再次被填滿,然后會針對0代對象區(qū)域進行新一輪的垃圾收集供置,之后這些0代對象又會列為第1代對象谨湘,并入第1代區(qū)域內(nèi)。第1代區(qū)域起初也會被設(shè)上一個容量限制值芥丧,等到第1代對象大小超過了這個限制之后紧阔,GC就會擴大戰(zhàn)場,對第1代區(qū)域也做一次垃圾收集续担,之后寓辱,又一次幸存下來的對象將會提升一個代齡,成為第2代對象赤拒。
可見,有一些對象雖然符合垃圾的所有條件诱鞠,但它們?nèi)绻堑?代(甚至是第2代老臣)對象挎挖,并且第1代的分配量還小于被設(shè)定的限制值時,這些垃圾對象就不會被GC發(fā)現(xiàn)航夺,并且可以繼續(xù)存活下去蕉朵。
另外,GC還會在工作過程中汲取經(jīng)驗阳掐,根據(jù)應(yīng)用程序的特點而自動調(diào)整每代對象區(qū)域的容量始衅,從而可以更高效的工作冷蚂。
應(yīng)該了解的垃圾收集機制(二)
對于大多數(shù)應(yīng)用而言,了解垃圾收集機制的主要動機并不是為了對內(nèi)存“省吃儉用”汛闸,而是為了處理非托管資源的控制問題蝙茶,這些問題往往跟內(nèi)存的大小沒有什么關(guān)系。例如對一個文件進行操作诸老,該何時關(guān)閉文件隆夯,關(guān)閉文件時要注意什么問題,如果忘了關(guān)閉會帶來什么后果别伏?這些都是我們需要認真考慮的蹄衷,無論你的內(nèi)存有多大:)
對于這一類的操作,我們不能依賴GC幫我們做厘肮,因為它并不知道我們在釋放時想干什么愧口,它甚至不知道自己該干什么!我們不得不自己動手來編寫處理代碼类茂。當然耍属,微軟已經(jīng)為我們搭好了框架,就是這兩個函數(shù):Finalize和Dispose大咱。它們也代表了非托管清理的兩種方式:自動和手動恬涧。
一. Finalize
Finalize很像C++的析構(gòu)函數(shù),我們在代碼中的實現(xiàn)形式為這與C++的析構(gòu)函數(shù)在形式上完全一樣碴巾,但它的調(diào)用過程卻大不相同溯捆。
~ClassName() {//釋放你的非托管資源}
比如類A中實現(xiàn)了Finalize函數(shù),在A的一個對象a被創(chuàng)建時(準確的說應(yīng)該是構(gòu)造函數(shù)被調(diào)用之前)厦瓢,它的指針被插入到一個finalization鏈表中提揍;在GC運行時,它將查找finalization鏈表中的對象指針煮仇,如果此時a已經(jīng)是垃圾對象的話劳跃,它會被移入一個freachable隊列中,最后GC會調(diào)用一個高優(yōu)先級線程浙垫,這個線程專門負責遍歷freachable隊列并調(diào)用隊列中所有對象的Finalize方法刨仑,至此,對象a中的非托管資源才得到了釋放(當然前提是你正確實現(xiàn)了它的Finalize方法)夹姥,而a所占用的內(nèi)存資源則必需等到下一次GC才能得到釋放杉武,所以一個實現(xiàn)了Finalize方法的對象必需等兩次GC才能被完全釋放。
由于Finalize是由GC負責調(diào)用辙售,所以可以說是一種自動的釋放方式轻抱。但是這里面要注意兩個問題:第一,由于無法確定GC何時會運作旦部,因此可能很長的一段時間里對象的資源都沒有得到釋放祈搜,這對于一些關(guān)鍵資源而言是非常要命的较店。第二,由于負責調(diào)用Finalize的線程并不保證各個對象的Finalize的調(diào)用順序容燕,這可能會帶來微妙的依賴性問題梁呈。如果你在對象a的Finalize中引用了對象b,而a和b兩者都實現(xiàn)了Finalize缰趋,那么如果b的Finalize先被調(diào)用的話捧杉,隨后在調(diào)用a的Finalize時就會出現(xiàn)問題,因為它引用了一個已經(jīng)被釋放的資源秘血。因此味抖,在Finalize方法中應(yīng)該盡量避免引用其他實現(xiàn)了Finalize方法的對象。
可見灰粮,這種“自動”釋放資源的方法并不能滿足我們的需要仔涩,因為我們不能顯示的調(diào)用它(只能由GC調(diào)用),而且會產(chǎn)生依賴型問題粘舟。我們需要更準確的控制資源的釋放熔脂。
二. Dispose
Dispose是提供給我們顯示調(diào)用的方法。由于對Dispose的實現(xiàn)很容易出現(xiàn)問題柑肴,所以在一些書籍上(如《Effective C#》和《Applied Microsoft.Net Framework Programming》)給出了一個特定的實現(xiàn)模式:
class DisposePattern :IDisposable
{
private System.IO.FileStream fs = new System.IO.FileStream("test.txt", System.IO.FileMode.Create);
~DisposePattern()
{
Dispose(false);
}
IDisposable Members#region IDisposable Members
public void Dispose()
{
//告訴GC不需要再調(diào)用Finalize方法霞揉,
//因為資源已經(jīng)被顯示清理
GC.SupdivssFinalize(this);
Dispose(true);
}
endregion
protected virtual void Dispose(bool disposing)
{
//由于Dispose方法可能被多線程調(diào)用,
//所以加鎖以確保線程安全
lock (this)
{
if (disposing)
{
//說明對象的Finalize方法并沒有被執(zhí)行晰骑,
//在這里可以安全的引用其他實現(xiàn)了Finalize方法的對象
}
if (fs != null)
{
fs.Dispose();
fs = null; //標識資源已經(jīng)清理适秩,避免多次釋放
}
}
}
}
在注釋中已經(jīng)有了比較清楚的描述,另外還有一點需要說明:如果DisposePattern類是派生自基類B硕舆,而B是一個實現(xiàn)了Dispose的類秽荞,那么DisposePattern中只需要override基類B的帶參的Dispose方法即可,而不需要重寫無參的Dispose和Finalize方法抚官,此時Dispose的實現(xiàn)為:
class DerivedClass : DisposePattern
{
protected override void Dispose(bool disposing)
{
lock (this)
{
try
{
//清理自己的非托管資源扬跋,
//實現(xiàn)模式與DisposePattern相同
}
finally
{
base.Dispose(disposing);
}
}
}
}
當然,如果DerivedClass本身沒有什么資源需要清理凌节,那么就不需要重寫Dispose方法了钦听,正如我們平時做的一些對話框,雖然都是繼承于System.Windows.Forms.Form倍奢,但我們常常不需要去重寫基類Form的Dispose方法彪见,因為本身沒有什么非托管的咚咚需要釋放。
了解GC的脾性在很多時候是非常必要的娱挨,起碼在出現(xiàn)資源泄漏問題的時候你不至于手足無措。我寫過一個生成excel報表的控件捕犬,其中對excel對象的釋放就讓我忙活了一陣跷坝。如果你做過excel開發(fā)的話酵镜,可能也遇到過結(jié)束excel進程之類的問題,特別是包裝成一個供別人調(diào)用的庫時柴钻,何時釋放excel對象以確保進程結(jié)束是一個關(guān)鍵問題淮韭。當然,GC的內(nèi)部機制非常復雜贴届,還有許多內(nèi)容可挖靠粪,但了解所有細節(jié)的成本太高,只需了解基礎(chǔ)毫蚓,夠用就好占键。