在.NET中,CLR通過記憶體回收(garbage collection)來管理已指派的對象,C#程式員從來不直接從記憶體刪除一個託管對象(C#語言中沒有delete關鍵字)。.NET對象被分配到一塊叫做託管堆(managed heap)的記憶體地區上,在那裡他們會在“將來某一時刻”被GC自動銷毀。在本文中,將說明C#中的對象生命週期、記憶體回收和建立可終結可處置的安全類型。
對象生命週期
類只是一個藍圖,描述這個類型的執行個體在記憶體中看起來是什麼樣子。定義了類後,就可以使用new關鍵字建立任意數量的對象,但是new關鍵字返回的是一個指向堆上對象的引用,而不是真正的對象本身。這個引用變數儲存在棧內,以供應用程式使用。預設情況下,一個參考型別的賦值將產生一個對該堆上同一個對象的新引用。
說明了類、對象和引用的關係:
當C#編譯器遇到new關鍵字時,它會在方法的實現中加入一條CIL newObj指令。CIL newObj指令的核心任務有:
a.計算指派至所需要的總記憶體數(包含類型的成員變數和類型的基類所需的必要記憶體);
b.檢查託管堆,確保有足夠的空間來放置要分配的對象。如果空間足夠,調用類型的建構函式,最終將記憶體中新對象的引用返回給調用者;
c.在將引用返回給調用者之前,移動下一個對象的指標(將分配的新對象的指標),指向託管堆上的下一個可用的位置。
該基本過程如所示:
當處理newObj指令時,如果CLR判定託管堆沒有足夠的空間來分配所請求的類型,它會執行一次記憶體回收來嘗試釋放記憶體。
對象建立後,對於實值型別的對象,當它們越出定義的範圍時(如:方法內的臨時變數,在方法返回後),這個類型的對象就消亡了;對於參考型別的對象,它們分配在託管堆上,這些對象一直保留在記憶體中,直到.NET記憶體回收行程將它們銷毀。
記憶體回收
如前所述,參考型別的對象由new建立後,記憶體回收行程將在不再需要時才將其銷毀。問題是記憶體回收行程如何判斷一個對象什麼時候不再需要呢?簡單的說是,當一個對象從程式碼程式庫的任何部分都不可達(即對象沒有根)時,記憶體回收行程就將其標記為垃圾,成為記憶體回收的候選目標。
為了最佳化CLR尋找不可達對象的過程,堆上的每一個對象被指定為屬於某“代(generation)”,代的思路很簡單:對象在堆上存在的時間越長,它就更應該保留(如實現Main方法的對象,應用程式物件),相反最近才放在堆上的對象可能很快就不可達了(如在方法範圍中建立的對象,本地變數)。基於此,每一個堆上的對象都屬於下列某代:
第0代:從沒有被標記為回收的新分配的對象
第1代:在上一次記憶體回收中沒有被回收的對象(即它被標記為回收,但因為已經擷取了足夠的堆空間而沒有被回收)
第2代:在一次以上的記憶體回收後仍然沒有被回收的對象
記憶體回收時,記憶體回收行程首先處理第0代對象,如果不夠再處理第1代、甚至第2代對象,剩餘的第0代對象提升為第1代,以此類推,但上限為第2代。
當確實發生回收時,記憶體回收行程暫時掛起所有再當前進程中所有活動的線程,以保證應用程式在回收過程中不會訪問堆,一旦記憶體回收周期完成,就允許掛起的線程繼續它們的工作。
構建可終結類型
在System.Object類中,定義了Finalize()虛方法,該方法的作用是保證.NET對象能在記憶體回收時清除非託管資源。非託管資源(如原始的檔案控制代碼、資料庫連接等)是通過使用PInvoke(平台叫用)服務直接叫用作業系統的API,或通過一些複雜的COM互動獲得的。顯然該方法的預設實現什麼也不做。
由於有記憶體回收,大多數C#類都不需要重寫Finalize()方法來顯式的指定清理邏輯,一切託管對象最終都會被記憶體回收。只是在使用非託管資源時,才可能需要自訂清理邏輯。Finalize()方法是受保護的,所以不可能直接調用一個對象的Finalize()方法,在從記憶體回收這個對象之前,記憶體回收行程會自動調用對象的Finalize()方法(如果支援的話),細節見後面的描述。
註:在結構類型上重寫Finalize()是不合法的,因為結構是實值型別,它們本來就從不分配在堆上
在C#中重寫Finalize()方法比較奇怪,不能用預期的override關鍵字來做:
- public class MyResourceWrapper
- {
- //編譯器錯誤!
- protected
override void Finalize() {}
- }
當想重寫Finalize()方法時,可以使用如下的解構函式文法。
- class MyResourceWrapper
- {
- ~MyResourceWrapper()
- {
- //這裡清除非託管資源,這裡僅是測試
- Console.Beep()
- }
- }
之所以用這種替代形式,是因為C#編譯器處理一個析造函數時,它將自動在Finalize方法中增加必需的錯誤偵測代碼,保證基類的Finalize()方法總是被執行。如果用ildasm.exe查看這個C#解構函式,將看到:
- .method family hidebysig virtual instance
void
- Finalize() cil managed
- {
- // Code size 13 (0xd)
- .maxstack 1
- .try
- {
- IL_0000: ldc.i4 0x4e20
- IL_0005: ldc.i4 0x3e8
- IL_000a: call
- void [mscorlib]System.Console::Beep(int32, int32)
- IL_000f: nop
- IL_0010: nop
- IL_0011: leave.s IL_001b
- } // end .try
- finally
- {
- IL_0013: ldarg.0
- IL_0014:
- call instance void [mscorlib]System.Object::Finalize()
- IL_0019: nop
- IL_001a: endfinally
- } // end handler
- IL_001b: nop
- IL_001c: ret
- } // end of method MyResourceWrapper::Finalize
對於沒有使用非託管資源的類型,終結是沒有用的。事實上,只要有可能的話,就應該在設計類型時避免提供Finalize()方法,因為終結是要花費時間的。
當在託管堆上指派至時,運行庫自動確定該對象是否提供一個自訂的Finalize()方法,如果是則對象被標記為可終結的,同時一個指向該對象的指標被儲存在記憶體回收行程維護的內部隊列(終結隊列)中。當記憶體回收行程確定釋放一個對象時,它檢查終結隊列上的每一個項,並將對象從堆上複製到另外一個託管結構終結可達表上。下一個記憶體回收時將產生另一個線程,為每一個在可達表中的對象調用Finalize()方法。因此,為了真正終結一個對象,至少要進行兩次記憶體回收。總而言之,儘管對象的終結能夠保證對象可以清除非託管資源,但它本質上仍然時非確定的,二期由於額外的幕後處理,速度會變得相當慢。
構建可處置類型
很多非託管資源都非常寶貴,所以它們應該儘可能快地被清除,固然可以通過Finalize()方法在記憶體回收時清除這些資源,但是這樣會有將對象放在終結隊列上的效能損失,而且必須等待記憶體回收行程觸發類的終結邏輯。因此需要另外一種處理對象清理工作的技術--實現IDisposable介面,該介面定義了一個名為Dispose()的方法,可以在該方法內自訂必要的對象清理邏輯。
實現IDisposable介面,就是假設當對象使用者不再使用這個對象時,會在這個對象引用離開範圍之前手工地調用Dispose(),這樣對象可以執行非託管資源的清理。與Finalize()方法不同,在Dispose方法中與其他託管對象通訊時很安全的,因為記憶體回收行程並不支援IDisposable介面,永遠不會調用Dispose(),因此當對象的使用者調用這個方法時,對象仍然在託管堆上,並可以訪問所有其他分配在堆上的對象。
註:與重寫Finalize()不同,結構和類類型都可以支援IDisposable
處理實現了IDisposable的託管對象時,為保證類型的Dispose()方法在出現運行時異常時也會調用,通常需要把每個可處置的類型封裝在try/catch/finally塊中。為此,C#提供特殊的文法,如下所示:
- static void Main()
- {
- //當退出using範圍時,自動調用Dispose()
- using(MyResourceWrapper rw =
new MyResourceWrapper())
- {
- //使用rw對象
- }
- }
- //如果用ildasm.exe查看Main()方法的CIL代碼,會發現using文法確實用Dispose()調
- //用擴充了try/finally邏輯:
- .method private hidebysig
static void Main(string[] args) cil managed
- {
- ...
- .try
- {
- ...
- } // end .try
- finally
- {
- ...
- IL_0012: callvirt instance void
- SimpleFinalize.MyResourceWrapper::Dispose()
- } // end handler
- ...
- } // end of method Program::Main
構建可終結可處置類型
現在有兩種方式來構造能夠清理內部非託管資源的類,其一可以重寫Finalize()方法,其二可以實現IDisposable。前者盡可以放心,因為對象可以不需要使用者參與進行記憶體回收,清除它自身;後者給對象使用者提供一種一旦對象用完就能清除的方法,但是如果使用者忘記調用Dispose(),非託管資源可能會永遠留在記憶體中。
同時採用上述兩種技術是可行的,可以獲得兩種模型的好處,如果對象使用者調用Dispose(),可以通過調用GC.SuppressFinalize()通知記憶體回收行程跳過終結過程;如果忘記調用Dispose(),對象最終也將被終結。這樣的做法確實能很好地工作,但還有小缺陷,首先Finalize()和Dispose()方法都有相同的清除非託管資源的代碼,應該定義一個私人的輔助函數供兩個方法調用。另外,還要確保Finalize()方法不會嘗試處置任何託管對象,而Dispose()方法則應該這樣做。最後還要確保對象使用者可以安全地多次調用Dispose()而不出錯誤。
為了實現這樣的設計,微軟定義了一個正式的可終結可處置模式,它在健壯性、可維護性和效能三者之間取得了平衡。下面是這個模式的樣本:
- public class MyResourceWrapper : IDisposable
- {
- // Used to determine if Dispose() has already been called.
- // 用來判斷Dispose()是否已經被調用
- private bool disposed =
false;
-
- public void Dispose()
- {
- // Call our helper method.調用輔助方法
- // Specifying "true" signifies that
- // the object user triggered the clean up.
- // 指定true表示對象使用者觸發了清理過程
- Dispose(true);
-
- // Now suppress finialization.
- // 現在跳過終結
- GC.SuppressFinalize(this);
- }
- //protected void virtual Dispose(bool disposing)
- //也可以這樣定義該方法簽名
- private
void Dispose(bool disposing)
- {
- // Be sure we have not already been disposed!
- //確保還沒有被處置
- if (!this.disposed)
- {
- // If disposing equals true, dispose all
- // managed resources.如果disposing等於true,處置所有託管資源
- if (disposing)
- {
- // Dispose managed resources.處置託管的資源
- }
-
- // Clean up unmanaged resources here.在這裡清理非託管的資源
- }
- disposed = true;
- }
- ~MyResourceWrapper()
- {
- // Call our helper method.調用輔助方法
- // Specifying "false" signifies that
- // the GC triggered the clean up.
- // 指定false表示GC觸發了清理過程
- Dispose(false);
- }
- }
總結
至此,CLR通過記憶體回收行程管理對象的流程說明結束,當然不包括一些細節,如:弱引用(weak reference)和對象複蘇(object resurrection)等。終結全文,可以總結出如下幾條規則:
法則1. 使用new關鍵字將一個對象分配在託管堆上,然後就不用再管;
法則2. 如果託管堆沒有足夠的記憶體來分配所請求的對象,就會進行記憶體回收;
法則3. 重寫Finalize()的唯一原因是,C#類通過PInvoke或複雜的COM互通性任務使用了非託管資源(典型情況是通過System.Runtime.InteropServices.Marshal類型);
法則4. 如果對象支援IDisposable,總是要對任何直接建立的對象調用Dispose()。應該認為,如果類設計者選擇支援Dispose()方法,這個類型就需要執行清除工作;