面試出現頻率:經常出現,但通常不會問的十分深入。通常來說,看完我這篇文章就足夠應付面試了。面試時主要考察垃圾回收的基本概念,標記-壓縮算法,以及對於微軟的垃圾回收模板的理解。知道什么時候需要繼承IDisposible接口,解構函數是做什么用的,什么時候需要自己寫一個解構函數。
重要程度:10/10
參考書籍:CLR via C#,其對垃圾回收講解的十分詳細,有些內容甚至過於高深。熟悉垃圾回收可以使你的程序更加健壯,性能更好。
4.1 托管堆的構造
垃圾回收的主要操作對象是托管堆,托管堆包括GC堆和加載堆。
GC堆里面為了提高內存管理效率等因素,分成多個部分,其中兩個主要部分為:
- 0/1/2代:越大的代的堆空間越大。
- 大對象堆(Large Object Heap),大於85000字節的大對象會分配到這個區域,這個區域的主要特點就是:不會輕易被回收;就是回收了也不會被壓縮(因為對象太大,移動復制的成本太高)。大對象堆是第二代GC堆的一部分。
加載堆不受GC管轄。加載堆上的主要對象有類型對象和它們的靜態字段,字符串駐留池等。幾個非托管資源的例子:StreamWriter,數據庫連接對象等。
4.2 關於垃圾
- 垃圾是不會再被用到的資源。具體的情況則包括超出該變量的有效范圍(離開了對應的大括號的區域變量),將變量指定為null,重新指向其他物件(而原先指向的物件已無法被取得),重新初始化等,這時原先變量占有的空間都會被CLR視為垃圾而等待回收。
- 托管代碼/資源/物件是會被CLR管理的代碼(CLR會對它們進行內存管理,垃圾回收,線程管理等),反之則是非托管代碼。
- C#的值類型(如果它屬於托管代碼)存儲在棧中。使用完(離開其作用域)就立刻銷毀。
- C#的引用類型(如果它屬於托管代碼)存儲在棧和堆中。使用完(離開其作用域)棧上的資料立刻銷毀,而堆上(棧上所引用的資料指向堆上的一塊空間)的資料不立刻銷毀。銷毀時間根據其世代而定。
4.3 簡述GC的垃圾回收策略
- GC將整個托管堆分成0代,1代和2代三個區域。更高的世代的區域更大。所有的引用對象一開始都是在第0代分配地址。進行垃圾回收時,大部分情況都是只對某個特定代進行操作。這樣分配基於下面幾個假設:
- 越老的對象生存期越長(即還可能繼續生存很長一段時間)
- 回收堆的一部分快於回收整個堆
- 當程序調用new操作符創建對象時,會計算類型(及其所有基類型)的字段需要的字節數。如果托管堆已經沒有足夠的空間來創建新對象了(第0代滿),就觸發一次垃圾回收。
- 整個回收將會遍歷0,1,2三代區域,並先標記,后壓縮,標記了的所有0代垃圾被銷毀,幸存者移到第一代堆中。標記了的所有1代垃圾被銷毀,幸存者被移到第2代堆中。所有第二代堆的垃圾將會被銷毀。幸存者仍然在第2代堆中。
- GC使用的垃圾回收算法是先標記(垃圾),之后壓縮,將垃圾清理,釋放,將幸存者升代,使得垃圾釋放空出來的位置變得連續。類似於磁盤空間的碎片整理。連續的空間便於管理和建立新的對象。
- 具體一點說,每個應用程序都包含一組根,每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要么引用堆中的一個對象,要么為null。
- GC開始執行時,假設堆上所有的對象都是垃圾。在標記階段,GC沿着線程棧開始遍歷,檢查每個根是否為null。對於那些有引用對象的根,則不認為它們是垃圾。
- 可以通過呼叫GC.Collect來主動觸發一次垃圾回收(甚至可以指定某代),但通常這是沒必要的。
4.4 何時需要繼承IDisposible接口?
你可以繼承IDisposible接口,然后在Dispose方法中銷毀任何資源,包括非托管資源。但如果你忘記了調用它,那么你的非托管資源將沒有任何機會得到釋放。只有當你的類型含有非托管資源,或者實現了IDisposible的托管資源時,你才需要繼承IDisposible接口,實現一個Dispose。 如果你只面對一堆托管資源,並且它們都沒有實現IDisposible時,你不需要做任何事。
4.5 什么是Finalize方法?
- 只要對象繼承自Object,它就擁有Finalize方法。在創建這個對象時,會在Finalization Queue(終結列表,由垃圾回收器控制的一個內部數據結構)為其加入一個指針。擁有Finalize方法的對象被稱為可終結的。
- Finalize方法又被稱為終結器。復寫Finalize方法稱為實現終結器。只有你需要釋放非托管資源時才需要這么做。
- 復寫Finalize方法的唯一方法是實現一個解構函數。解構函數的實現只有一個意義,就是保證非托管資源得到回收,作為Dispose這道關口后面的最終總閘,因為解構函數是肯定會被執行到的。
- 垃圾清理時,會標記所有的垃圾,並探查終結列表,並將其中為垃圾的對象移除出終結列表,加到Freachable Queue之中(這無形當中會給對象續命一輪GC,因為此時對象被Freachable Queue引用,不再是沒有被任何其他對象引用的垃圾)。
- 一個特殊的高優先級的線程專門負責調用Finalize方法。這可以避免潛在的線程同步問題。Freachable隊列為空時,該線程睡眠。一旦Freachable隊列有記錄出現,該線程就會被喚醒,將每一項都從Freachable隊列中移出,並調用每一項對象的Finalize方法,該方法會銷毀對象。
- 當GC隱式的處理垃圾回收時,第一輪GC會將所有的擁有Finalize方法的垃圾移動到Freachable Queue之中,並不調用Finalize方法(所以對象還活着)。下一輪GC才遍歷上面那輪GC中,放到Freachable Queue的對象,並使用Finalize方法銷毀那些引用類型對象。所以如果對象擁有Finalize方法,它的壽命會無形之中延長一輪GC(稱為對象的復生),並且它的Finalize方法調用的時間是不可知的。在必要的時候,你可以實現IDisposible接口,利用Dispose來主動銷毀資源,並在Dispose()成功地執行之后呼叫GC.SuppressFinalize(this); 這可以告訴GC不需再去呼叫這個物件的Finalize方法(因為Dispose執行過了之后Finalize不需要執行了),這樣GC就不會把對象從終結列表移動到freachable隊列,可以回避系統的續命行為。
- 因為終結器會導致續命,所以請留心,記得呼叫Dispose,並呼叫GC.SuppressFinalize(this),這可以讓終結器沒有機會上場,對象就被銷毀了。
4.6 什么是解構函數?何時需要寫一個解構函數?
- 解構函數是Finalize方法的override。它將會被隱式的轉換為一個帶有try-finally的Finalize方法,覆蓋它的父對象的Finalize,並在finally中呼叫base.Finalize。(此處的base指System.Object)
- 解構函數不能有參數和方法修飾符。除非你主動觸發垃圾回收,它的執行時間是不可知的。
- 雖然僅由托管資源組成的類型也可能會因為用戶忘了呼叫Dispose而暫時存留在堆中,這並不會造成太大的問題,因為GC最終會回收它。而如果類型中有非托管資源,你需要實現解構函數。如果你沒有實現解構函數,又忘了呼叫Dispose,則當GC回收這個類型時(通過Finalize),將只會回收托管資源(非托管資源沒有Finalize方法),非托管資源將會一直存留在堆中。
4.7 如何回收托管資源?
如果類型沒有非托管資源,此時,因為所有托管資源肯定都有Finalize方法,我們不需要實現解構函數。特別的,對於實現了IDisposible的類型,我們只需要簡單的調用Dispose來釋放資源即可(這會調用那個類型的Dispose方法,如果類型是屬於微軟的,則微軟已經給你實現好了)。有些類型的Dispose方法的名稱為Close。
如果你的托管資源包含了一些實現了IDisposible接口的成員時,你要繼承IDisposible接口,並在Dispose方法中將這些成員回收。或者,你在使用成員時,使用using關鍵字。using關鍵字本質上是一個try - finally塊,所以即使你在using塊中發生了異常,也不用擔心,對象仍然會在finally塊中被dispose。(曾經有面試官問過我這個問題)
4.8 如何回收非托管資源?
如果你只是臨時使用非托管資源,那么將其包含在using中就可以了,例如使用StreamWriter。
假設你的類型中含有非托管資源屬性/字段,此時,你要繼承IDisposible接口,實現Dispose方法,並寫一個解構函數。你可以follow微軟的垃圾回收模板,步驟如下:
- 寫一個私有的方法,在私有的方法中,釋放托管資源(如果該資源擁有Dispose方法則可以通過呼叫它的Dispose方法完成)和非托管資源。
- 實現Dispose方法,呼叫私有方法,之后呼叫SuppressFinalize。
- 實現一個解構函數(這會覆蓋原有的Finalize方法)在其中呼叫私有方法。這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源。原有的Finalize方法並不會理會非托管資源。在解構函數中你不需要呼叫SuppressFinalize因為這已經是Finalize方法了,續命已經發生了。
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() { //實現Dispose方法,呼叫私有方法,之后呼叫SuppressFinalize this.CloseHandle(); GC.SuppressFinalize(this); } ~WindowStationHandle() { //實現一個解構函數(這會覆蓋原有的Finalize方法)在其中呼叫私有方法。 //這是為了防止用戶忘了呼叫Dispose方法而最終沒有回收這個非托管資源。 //原有的Finalize方法並不會理會非托管資源。 this.CloseHandle(); } }
4.10 垃圾回收策略
結合上面4.4,4.8和4.9,就構成了常規的垃圾回收策略:
- 類中沒有非托管資源,且沒有對象實現IDisposible: 什么也不用做。
- 類中沒有非托管資源,且有對象實現IDisposible: 特別注意這些對象,確保調用了它們的Dispose方法(顯式或者隱式的)。你可以實現IDisposible,然后實現Dispose方法,在其中釋放資源。
- 類中有非托管資源: 跟從微軟模板,實現一個私有函數釋放托管和非托管資源,實現IDisposible,然后實現Dispose方法,並在其中調用私有函數,然后呼叫GC.SuppressFinalize(第一道閘)。實現一個解構函數,並在其中調用私有函數(第二道閘)。如果你的第一道閘完美無缺,第二道閘是沒有機會上場的。
4.11 擴展閱讀:
大對象堆陷阱:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html