這裡說析構函數,其實並不准確,應該叫Finalize函數,Finalize函數形式上和c++的析構函數很像 ,都是(~ClassName)的形式,但是功能上完全不一樣。析構函數編譯成il語言後會變成一個Finalize的函數,他是重寫的object的Finalize虛函數,標題上用析構函數,主要是我認為很多人不知道Finalize函數。
寫一個類型解釋下可能會更通俗易懂一點:
public class Test { ~Test() { } //這個就是Finalize函數 private byte[] b = new byte[10000]; }
最近看了一些代碼,有不少用Finalize函數的。特別是ef數據倉庫中,情況如下:
public class DbRepostory { private Context context; public DbRepostoty(Context context) { this.context = context; } ~DbRepostory() { context.Dispose(); } } public class Context : DbContext { }
看上去很高大上,但是這樣寫到底好不好呢?好不好我們最後再去評論,先看一看下面這個簡單的例子:
public class WithFinalize { ~WithFinalize() { } private byte[] b = new byte[10000]; } public class WithoutFinalize { private byte[] b = new byte[10000]; } class Program { public static void Main(string[] args) { Console.WriteLine("測試1無Finalize函數:"); Test<WithoutFinalize>(); Console.WriteLine(Environment.NewLine+ "測試2有Finalize函數:"); Test<WithFinalize>(); Console.ReadKey(); } public static void Test<T>() where T : new() { GC.Collect(); Thread.Sleep(10); Console.WriteLine("初始內存:" + GC.GetTotalMemory(false)); var list = new List<T>(); for (int i = 0; i < 10; i++) list.Add(new T()); Console.WriteLine("分配之後:" + GC.GetTotalMemory(false)); GC.Collect(); Thread.Sleep(10); Console.WriteLine("一次回收:" + GC.GetTotalMemory(false)); GC.Collect(); Thread.Sleep(10); Console.WriteLine("二次回收:" + GC.GetTotalMemory(false)); } }
這段代碼有三個類一個是我們需要運行的主程序,另外兩個 WhitFinalize 和WhitoutFinalize則是我們要測試的類型,這兩個類一個加了Finalize函數,一個未加,其余的完全一樣。主程序則分別要測試這兩個類型在垃圾回收的時的表現,我們先測試的沒有加Finalize函數的類型,在測試的加了類型。 一共四個數值,分別是初始時的內存, new了10個測試類型之後的內存(測試類型大約需要10k的內存空間,10個也就是大約100k),垃圾回收一次之後的內存,垃圾回收二次之後的內存,我們看下具體的運行情況:
測試1無Finalize函數: 初始內存:96224 分配之後:196464 一次回收:97036 二次回收:97036 測試2有Finalize函數: 初始內存:97056 分配之後:197296 一次回收:197396 二次回收:97156
從運行情況來看兩次測試的初始化內存都大約97k左右,new了10個測試對象之後都增長了大約100k,和預期的一樣,但是第一次垃圾回收之後測試1(沒有Finalize函數)回收了100k左右的內存,而測試2(有Finalize函數)則基本上沒有回收掉內存,卻等到了第二次垃圾回收 回收了100k內存。不禁會想,這又是為什呢?
這得從垃圾回收的一些原理說起,東西比較多,我們說的簡單一下。垃圾回收的時候會從根遍歷所有引用的對象,然後遍歷到了就做好標記,代表有用,沒遍歷到的就會是為垃圾,但是在這些垃圾中有一些對象定義了Finalize函數,於是就把這些有Finalize的對象從垃圾堆裡拉了回來,其余的垃圾則回收掉,而這些死而復活的對象則和那些本來就不是垃圾對象都幸存了下來,並一並升級為下一代對象,垃圾回收結束之後 clr會用一個較高優先級的線程來調用這些死而復活對象的Finalize方法,直到下次垃圾回收他們才被回收掉。這也是我們看到測試2第二次垃圾回收才被回收掉的原因,我們在這裡講的都是一些粗略的東西,內部實現還要復雜。
我們看到我在代碼裡用到了很多Thread.Sleep(10); 這是什麼原因呢?這就的注意下我上一段的一句話“垃圾回收結束之後 clr會用一個較高優先級的線程來調用這些死而復活對象的Finalize方法”,Finalize方法的調用和我們的前台代碼是並發進行的,而且我們前台代碼比較簡單,如果不暫停一下的話很可能不少對象的Finalize方法還沒執行完,我們就調用了下一次的垃圾回收(GC.Collect())。影響結果的准確性。
還有我們之前提到了代的概念,這裡也簡單說一下代,垃圾回收時對象一共有三代 :0,1,2。每一代都有自己的內存預算,空間不足的時候會調用垃圾回收。為了提高性能都是按代回收,第0代超預算之後就回收第0代的對象,而存活下來的對象就提升為第1代,依次類推,而往往經過多次0代的垃圾回收才能回收一次第1代。
我們代碼中的GC.Collect();沒有參數,意思是回收所有代的對象,我們可以把GC.Collect()換成GC.Collect(0);意思是回收第0代的對象,然後運行程序:
public static void Test<T>() where T : new() { GC.Collect(); Thread.Sleep(10); Console.WriteLine("初始內存:" + GC.GetTotalMemory(false)); var list = new List<T>(); for (int i = 0; i < 10; i++) list.Add(new T()); Console.WriteLine("分配之後:" + GC.GetTotalMemory(false)); GC.Collect(0); Thread.Sleep(10); Console.WriteLine("一次回收:" + GC.GetTotalMemory(false)); GC.Collect(0); Thread.Sleep(10); Console.WriteLine("二次回收:" + GC.GetTotalMemory(false)); }
測試1無Finalize函數: 初始內存:96224 分配之後:196464 一次回收:97056 二次回收:97036 測試2有Finalize函數: 初始內存:97056 分配之後:197296 一次回收:197396 二次回收:197396
我們看到測試2中在第二次垃圾回收之後(對第0代)內存依舊沒有回收掉,而這種情況更接近於實際。
從上面的小例子中我們了解到Finalize方法對性能和內存都有不好的影響,那為什麼要存在這個方法呢?這裡我們說一下要使用Finalize的兩個情況:
第一個情況就是對象含有一個本機資源,比如一個句柄,這樣可以在Finalize方法釋放這個句柄,就能消除忘記釋放句柄造成的本機資源浪費。
第二種情況就是在這個對象被回收之前需要做一些必須要做的是事情,比如FileStream這個類,需要在回收之前把緩沖區的東西寫入到文件內。
我們在回過頭開看一看之前提到的數據倉庫的類,這個類第一沒有占用任何本機資源,第二在被回收之前也沒有必須要做的事情,寫一個Finalize方法並調用 context.Dispose(); 只能增加性能開銷,影響垃圾回收效果。我們可以用反編譯軟件看一下DbContext這個基類,他都沒有Finalize方法,又何必再畫蛇添足呢?
希望覺得對自己有幫助的朋友給我點個贊(●'?'●)