面試出現(xiàn)頻率:經(jīng)常出現(xiàn),但通常不會問的十分深入但汞。通常來說宿刮,看完我這篇文章就足夠應(yīng)付面試了互站。面試時主要考察垃圾回收的基本概念私蕾,標(biāo)記-壓縮算法,以及對于微軟的垃圾回收模板的理解胡桃。知道什么時候需要繼承IDisposible接口踩叭,解構(gòu)函數(shù)是做什么用的,什么時候需要自己寫一個解構(gòu)函數(shù)翠胰。
重要程度:10/10
參考書籍:CLR via C#容贝,其對垃圾回收講解的十分詳細(xì),有些內(nèi)容甚至過于高深之景。熟悉垃圾回收可以使你的程序更加健壯斤富,性能更好。
4.1 托管堆的構(gòu)造
垃圾回收的主要操作對象是托管堆锻狗,托管堆包括GC堆和加載堆满力。
GC堆里面為了提高內(nèi)存管理效率等因素,分成多個部分轻纪,其中兩個主要部分為:
- 0/1/2代:越大的代的堆空間越大油额。
- 大對象堆(Large Object Heap),大于85000字節(jié)的大對象會分配到這個區(qū)域刻帚,這個區(qū)域的主要特點就是:不會輕易被回收潦嘶;就是回收了也不會被壓縮(因為對象太大,移動復(fù)制的成本太高)崇众。大對象堆是第二代GC堆的一部分掂僵。
加載堆不受GC管轄。加載堆上的主要對象有類型對象和它們的靜態(tài)字段顷歌,字符串駐留池等锰蓬。幾個非托管資源的例子:StreamWriter,數(shù)據(jù)庫連接對象等衙吩。
4.2 關(guān)于垃圾
垃圾是不會再被用到的資源互妓。具體的情況則包括超出該變量的有效范圍(離開了對應(yīng)的大括號的區(qū)域變量),將變量指定為null,重新指向其他物件(而原先指向的物件已無法被取得)冯勉,重新初始化等澈蚌,這時原先變量占有的空間都會被CLR視為垃圾而等待回收。
托管代碼/資源/物件是會被CLR管理的代碼(CLR會對它們進(jìn)行內(nèi)存管理灼狰,垃圾回收宛瞄,線程管理等),反之則是非托管代碼交胚。
C#的值類型(如果它屬于托管代碼)存儲在棧中份汗。使用完(離開其作用域)就立刻銷毀。
C#的引用類型(如果它屬于托管代碼)存儲在棧和堆中蝴簇。使用完(離開其作用域)棧上的資料立刻銷毀杯活,而堆上(棧上所引用的資料指向堆上的一塊空間)的資料不立刻銷毀。銷毀時間根據(jù)其世代而定熬词。
4.3 簡述GC的垃圾回收策略
GC將整個托管堆分成0代旁钧,1代和2代三個區(qū)域。更高的世代的區(qū)域更大互拾。所有的引用對象一開始都是在第0代分配地址歪今。進(jìn)行垃圾回收時,大部分情況都是只對某個特定代進(jìn)行操作颜矿。這樣分配基于下面幾個假設(shè):
1.越老的對象生存期越長(即還可能繼續(xù)生存很長一段時間)
2.回收堆的一部分快于回收整個堆當(dāng)程序調(diào)用new操作符創(chuàng)建對象時寄猩,會計算類型(及其所有基類型)的字段需要的字節(jié)數(shù)。如果托管堆已經(jīng)沒有足夠的空間來創(chuàng)建新對象了(第0代滿)骑疆,就觸發(fā)一次垃圾回收田篇。
整個回收將會遍歷0,1,2三代區(qū)域,并先標(biāo)記封断,后壓縮斯辰,標(biāo)記了的所有0代垃圾被銷毀,幸存者移到第一代堆中坡疼。標(biāo)記了的所有1代垃圾被銷毀彬呻,幸存者被移到第2代堆中。所有第二代堆的垃圾將會被銷毀柄瑰。幸存者仍然在第2代堆中闸氮。
GC使用的垃圾回收算法是先標(biāo)記(垃圾),之后壓縮教沾,將垃圾清理法瑟,釋放腥沽,將幸存者升代挫剑,使得垃圾釋放空出來的位置變得連續(xù)。類似于磁盤空間的碎片整理孙咪。連續(xù)的空間便于管理和建立新的對象。
具體一點說巡语,每個應(yīng)用程序都包含一組根翎蹈,每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針男公。該指針要么引用堆中的一個對象荤堪,要么為null。
GC開始執(zhí)行時枢赔,假設(shè)堆上所有的對象都是垃圾澄阳。在標(biāo)記階段,GC沿著線程棧開始遍歷踏拜,檢查每個根是否為null碎赢。對于那些有引用對象的根,則不認(rèn)為它們是垃圾执隧。
可以通過呼叫GC.Collect來主動觸發(fā)一次垃圾回收(甚至可以指定某代)揩抡,但通常這是沒必要的户侥。
4.4 何時需要繼承IDisposible接口镀琉?
你可以繼承IDisposible接口,然后在Dispose方法中銷毀任何資源蕊唐,包括非托管資源屋摔。但如果你忘記了調(diào)用它,那么你的非托管資源將沒有任何機會得到釋放替梨。只有當(dāng)你的類型含有非托管資源钓试,或者實現(xiàn)了IDisposible的托管資源時,你才需要繼承IDisposible接口副瀑,實現(xiàn)一個Dispose弓熏。 如果你只面對一堆托管資源,并且它們都沒有實現(xiàn)IDisposible時糠睡,你不需要做任何事挽鞠。
4.5 什么是Finalize方法?
只要對象繼承自O(shè)bject狈孔,它就擁有Finalize方法信认。在創(chuàng)建這個對象時,會在Finalization Queue(終結(jié)列表均抽,由垃圾回收器控制的一個內(nèi)部數(shù)據(jù)結(jié)構(gòu))為其加入一個指針嫁赏。擁有Finalize方法的對象被稱為可終結(jié)的。
Finalize方法又被稱為終結(jié)器油挥。復(fù)寫Finalize方法稱為實現(xiàn)終結(jié)器潦蝇。只有你需要釋放非托管資源時才需要這么做款熬。
復(fù)寫Finalize方法的唯一方法是實現(xiàn)一個解構(gòu)函數(shù)。解構(gòu)函數(shù)的實現(xiàn)只有一個意義攘乒,就是保證非托管資源得到回收华烟,作為Dispose這道關(guān)口后面的最終總閘,因為解構(gòu)函數(shù)是肯定會被執(zhí)行到的持灰。
垃圾清理時盔夜,會標(biāo)記所有的垃圾,并探查終結(jié)列表堤魁,并將其中為垃圾的對象移除出終結(jié)列表喂链,加到Freachable Queue之中(這無形當(dāng)中會給對象續(xù)命一輪GC,因為此時對象被Freachable Queue引用妥泉,不再是沒有被任何其他對象引用的垃圾)椭微。
一個特殊的高優(yōu)先級的線程專門負(fù)責(zé)調(diào)用Finalize方法。這可以避免潛在的線程同步問題盲链。Freachable隊列為空時蝇率,該線程睡眠。一旦Freachable隊列有記錄出現(xiàn)刽沾,該線程就會被喚醒本慕,將每一項都從Freachable隊列中移出,并調(diào)用每一項對象的Finalize方法侧漓,該方法會銷毀對象锅尘。
當(dāng)GC隱式的處理垃圾回收時,第一輪GC會將所有的擁有Finalize方法的垃圾移動到Freachable Queue之中布蔗,并不調(diào)用Finalize方法(所以對象還活著)藤违。下一輪GC才遍歷上面那輪GC中,放到Freachable Queue的對象纵揍,并使用Finalize方法銷毀那些引用類型對象顿乒。所以如果對象擁有Finalize方法,它的壽命會無形之中延長一輪GC(稱為對象的復(fù)生)泽谨,并且它的Finalize方法調(diào)用的時間是不可知的璧榄。在必要的時候,你可以實現(xiàn)IDisposible接口隔盛,利用Dispose來主動銷毀資源犹菱,并在Dispose()成功地執(zhí)行之后呼叫GC.SuppressFinalize(this); 這可以告訴GC不需再去呼叫這個物件的Finalize方法(因為Dispose執(zhí)行過了之后Finalize不需要執(zhí)行了),這樣GC就不會把對象從終結(jié)列表移動到freachable隊列吮炕,可以回避系統(tǒng)的續(xù)命行為腊脱。
因為終結(jié)器會導(dǎo)致續(xù)命,所以請留心龙亲,記得呼叫Dispose陕凹,并呼叫GC.SuppressFinalize(this)悍抑,這可以讓終結(jié)器沒有機會上場,對象就被銷毀了杜耙。
4.6 什么是解構(gòu)函數(shù)搜骡?何時需要寫一個解構(gòu)函數(shù)?
解構(gòu)函數(shù)是Finalize方法的override佑女。它將會被隱式的轉(zhuǎn)換為一個帶有try-finally的Finalize方法记靡,覆蓋它的父對象的Finalize,并在finally中呼叫base.Finalize团驱。(此處的base指System.Object)
解構(gòu)函數(shù)不能有參數(shù)和方法修飾符摸吠。除非你主動觸發(fā)垃圾回收,它的執(zhí)行時間是不可知的嚎花。
雖然僅由托管資源組成的類型也可能會因為用戶忘了呼叫Dispose而暫時存留在堆中寸痢,這并不會造成太大的問題,因為GC最終會回收它紊选。而如果類型中有非托管資源啼止,你需要實現(xiàn)解構(gòu)函數(shù)。如果你沒有實現(xiàn)解構(gòu)函數(shù)兵罢,又忘了呼叫Dispose献烦,則當(dāng)GC回收這個類型時(通過Finalize),將只會回收托管資源(非托管資源沒有Finalize方法)趣些,非托管資源將會一直存留在堆中仿荆。
4.7 如何回收托管資源?
如果類型沒有非托管資源坏平,此時,因為所有托管資源肯定都有Finalize方法锦亦,我們不需要實現(xiàn)解構(gòu)函數(shù)舶替。特別的,對于實現(xiàn)了IDisposible的類型杠园,我們只需要簡單的調(diào)用Dispose來釋放資源即可(這會調(diào)用那個類型的Dispose方法顾瞪,如果類型是屬于微軟的,則微軟已經(jīng)給你實現(xiàn)好了)抛蚁。有些類型的Dispose方法的名稱為Close陈醒。
如果你的托管資源包含了一些實現(xiàn)了IDisposible接口的成員時,你要繼承IDisposible接口瞧甩,并在Dispose方法中將這些成員回收钉跷。或者肚逸,你在使用成員時爷辙,使用using關(guān)鍵字彬坏。using關(guān)鍵字本質(zhì)上是一個try - finally塊,所以即使你在using塊中發(fā)生了異常膝晾,也不用擔(dān)心栓始,對象仍然會在finally塊中被dispose。(曾經(jīng)有面試官問過我這個問題)
4.8 如何回收非托管資源血当?
如果你只是臨時使用非托管資源幻赚,那么將其包含在using中就可以了,例如使用StreamWriter臊旭。
假設(shè)你的類型中含有非托管資源屬性/字段坯屿,此時,你要繼承IDisposible接口巍扛,實現(xiàn)Dispose方法领跛,并寫一個解構(gòu)函數(shù)。你可以follow微軟的垃圾回收模板撤奸,步驟如下:
1.寫一個私有的方法吠昭,在私有的方法中,釋放托管資源(如果該資源擁有Dispose方法則可以通過呼叫它的Dispose方法完成)和非托管資源胧瓜。
2.實現(xiàn)Dispose方法矢棚,呼叫私有方法,之后呼叫SuppressFinalize府喳。
3.實現(xiàn)一個解構(gòu)函數(shù)(這會覆蓋原有的Finalize方法)在其中呼叫私有方法蒲肋。這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源。原有的Finalize方法并不會理會非托管資源钝满。在解構(gòu)函數(shù)中你不需要呼叫SuppressFinalize因為這已經(jīng)是Finalize方法了兜粘,續(xù)命已經(jīng)發(fā)生了。
public sealed class WindowStationHandle : IDisposable
{
// 非托管資源
public IntPtr Handle { get; set; }
public WindowStationHandle(IntPtr handle)
{
this.Handle = handle;
}
public WindowStationHandle()
: this(IntPtr.Zero)
{
}
public bool IsInvalid
{
get { return (this.Handle == IntPtr.Zero); }
}
// 私有方法
private void CloseHandle()
{
if (this.IsInvalid)
{
return;
}
if (!NativeMethods.CloseWindowStation(this.Handle))
{
Trace.WriteLine("CloseWindowStation: " + new Win32Exception().Message);
}
// 釋放非托管資源
this.Handle = IntPtr.Zero;
}
public void Dispose()
{
//實現(xiàn)Dispose方法弯蚜,呼叫私有方法孔轴,之后呼叫SuppressFinalize
this.CloseHandle();
GC.SuppressFinalize(this);
}
~WindowStationHandle()
{
//實現(xiàn)一個解構(gòu)函數(shù)(這會覆蓋原有的Finalize方法)在其中呼叫私有方法。
//這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源碎捺。
//原有的Finalize方法并不會理會非托管資源路鹰。
this.CloseHandle();
}
}
4.9 垃圾回收策略
結(jié)合上面4.4,4.8收厨,就構(gòu)成了常規(guī)的垃圾回收策略:
類中沒有非托管資源晋柱,且沒有對象實現(xiàn)IDisposible: 什么也不用做。
類中沒有非托管資源诵叁,且有對象實現(xiàn)IDisposible: 特別注意這些對象雁竞,確保調(diào)用了它們的Dispose方法(顯式或者隱式的)。你可以實現(xiàn)IDisposible黎休,然后實現(xiàn)Dispose方法浓领,在其中釋放資源玉凯。
類中有非托管資源: 跟從微軟模板,實現(xiàn)一個私有函數(shù)釋放托管和非托管資源联贩,實現(xiàn)IDisposible漫仆,然后實現(xiàn)Dispose方法,并在其中調(diào)用私有函數(shù)泪幌,然后呼叫GC.SuppressFinalize(第一道閘)盲厌。實現(xiàn)一個解構(gòu)函數(shù),并在其中調(diào)用私有函數(shù)(第二道閘)祸泪。如果你的第一道閘完美無缺吗浩,第二道閘是沒有機會上場的。
4.10 擴(kuò)展閱讀:
大對象堆陷阱:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html