以前忘了說了 代碼都是在Release模式下運行的,如今補充上。c++
這裏說析構函數,其實並不許確,應該叫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 { }
看上去很高大上,可是這樣寫到底好很差呢?好很差咱們最後再去評論,先看一看下面這個簡單的例子:post
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第二次垃圾回收才被回收掉的緣由,咱們在這裏講的都是一些粗略的東西,內部實現還要複雜。this
咱們看到我在代碼裏用到了不少Thread.Sleep(10); 這是什麼緣由呢?這就的注意下我上一段的一句話「垃圾回收結束以後 clr會用一個較高優先級的線程來調用這些死而復活對象的Finalize方法」,Finalize方法的調用和咱們的前臺代碼是併發進行的,並且咱們前臺代碼比較簡單,若是不暫停一下的話極可能很多對象的Finalize方法還沒執行完,咱們就調用了下一次的垃圾回收(GC.Collect())。影響結果的準確性。spa
還有咱們以前提到了代的概念,這裏也簡單說一下代,垃圾回收時對象一共有三代 :0,1,2。每一代都有本身的內存預算,空間不足的時候會調用垃圾回收。爲了提升性能都是按代回收,第0代超預算以後就回收第0代的對象,而存活下來的對象就提高爲第1代,依次類推,而每每通過屢次0代的垃圾回收才能回收一次第1代。線程
咱們代碼中的GC.Collect();沒有參數,意思是回收全部代的對象,咱們能夠把GC.Collect()換成GC.Collect(0);意思是回收第0代的對象,而後運行程序:code
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方法,又何須再多此一舉呢?
但願以爲對本身有幫助的朋友給我點個贊(●'◡'●)