在開始以前,咱們須要明確什麼是C#(或者說.NET)中的資源,打碼的時候咱們常常說釋放資源,那麼到底什麼是資源,簡單來說,C#中的每一種類型都是一種資源,而資源又分爲託管資源和非託管資源,那這又是什麼?!java
託管資源:由CLR管理分配和釋放的資源,也就是咱們直接new出來的對象;數據庫
非託管資源:不受CLR控制的資源,也就是不屬於.NET自己的功能,每每是經過調用跨平臺程序集(如C++)或者操做系統提供的一些接口,好比Windows內核對象、文件操做、數據庫鏈接、socket、Win32API、網絡等。緩存
咱們下文討論的,主要也就是非託管資源的釋放,而託管資源.NET的垃圾回收已經幫咱們完成了。其實非託管資源有部分.NET的垃圾回收也幫咱們實現了,那麼若是要讓.NET垃圾回收幫咱們釋放非託管資源,該如何去實現。網絡
假設咱們要使用FileStream,咱們一般的作法是將其using起來,或者是更老式的try…catch…finally…這種作法,由於它的實現調用了非託管資源,因此咱們必須用完以後要去顯式釋放它,若是不去釋放它,那麼可能就會形成內存泄漏。dom
這聽上去貌似很簡單,但咱們編碼的時候可能不少時候會忽略掉釋放資源這個問題,.NET的垃圾回收又如何幫咱們釋放非託管資源,接下來咱們一探究竟吧,一個標準的釋放非託管資源的類應該去實現IDisposable接口:socket
public class MyClass:IDisposable { /// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary> public void Dispose() { } }
咱們實例化的時候就能夠將這個類using起來:函數
using(var mc = new MyClass()) { }
看上去很簡單嘛,可是,要是就這麼簡單的話,也沒有這篇文章的必要了。若是要實現IDisposable接口,咱們其實應該這樣作:優化
實現Dispose方法;this
提取一個受保護的Dispose虛方法,在該方法中實現具體的釋放資源的邏輯;編碼
添加析構函數;
添加一個私有的bool類型的字段,做爲釋放資源的標記
接下來,咱們來實現這樣的一個Dispose模式:
public class MyClass : IDisposable { /// <summary> /// 模擬一個非託管資源 /// </summary> private IntPtr NativeResource { get; set; } = Marshal.AllocHGlobal(100); /// <summary> /// 模擬一個託管資源 /// </summary> public Random ManagedResource { get; set; } = new Random(); /// <summary> /// 釋放標記 /// </summary> private bool disposed; /// <summary> /// 爲了防止忘記顯式的調用Dispose方法 /// </summary> ~MyClass() { //必須爲false Dispose(false); } /// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary> public void Dispose() { //必須爲true Dispose(true); //通知垃圾回收器再也不調用終結器 GC.SuppressFinalize(this); } /// <summary> /// 非必需的,只是爲了更符合其餘語言的規範,如C++、java /// </summary> public void Close() { Dispose(); } /// <summary> /// 非密封類可重寫的Dispose方法,方便子類繼承時可重寫 /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { if (disposed) { return; } //清理託管資源 if (disposing) { if (ManagedResource != null) { ManagedResource = null; } } //清理非託管資源 if (NativeResource != IntPtr.Zero) { Marshal.FreeHGlobal(NativeResource); NativeResource = IntPtr.Zero; } //告訴本身已經被釋放 disposed = true; } }
若是不是虛方法,那麼就頗有可能讓開發者在子類繼承的時候忽略掉父類的清理工做,因此,基於繼承體系的緣由,咱們要提供這樣的一個虛方法。
其次,提供的這個虛方法是一個帶bool參數的,帶這個參數的目的,是爲了釋放資源時區分對待託管資源和非託管資源,而實現自IDisposable的Dispose方法調用時,傳入的是true,而終結器調用的時候,傳入的是false,當傳入true時表明要同時處理託管資源和非託管資源;而傳入false則只須要處理非託管資源便可。
那爲何要區別對待託管資源和非託管資源?在這個問題以前,其實咱們應該先弄明白:託管資源須要手動清理嗎?不妨將C#的類型分爲兩類:一類實現了IDisposable,另外一類則沒有。前者咱們定義爲非普通類型,後者爲普通類型。非普通類型包含了非託管資源,實現了IDisposable,但又包含有自身是託管資源,因此不普通,對於咱們剛纔的問題,答案就是:普通類型不須要手動清理,而非普通類型須要手動清理。
而咱們的Dispose模式設計思路在於:若是顯式調用Dispose,那麼類型就該循序漸進的將本身的資源所有釋放,若是忘記了調用Dispose,那就假定本身的全部資源(哪怕是非普通類型)都交給GC了,因此不須要手動清理,因此這就理解爲何實現自IDisposable的Dispose中調用虛方法是傳true,終結器中傳false了。
同時咱們還注意到了,虛方法首先判斷了disposed字段,這個字段用於判斷對象的釋放狀態,這意味着屢次調用Dispose時,若是對象已經被清理過了,那麼清理工做就不用再繼續。
但Dispose並不表明把對象置爲了null,且已經被回收完全不存在了。但事實上,對象的引用還可能存在的,只是再也不是正常的狀態了,因此咱們明白有時候咱們調用數據庫上下文有時候爲何會報「數據庫鏈接已被釋放」之類的異常了。
因此,disposed字段的存在,用來表示對象是否被釋放過。
這句話讀起來可能有點繞啊,也就是說,若是對象的某些字段或屬性是IDisposable的子類,好比FileStream,那麼這個類也應該實現IDisposable。
以前咱們說過C#的類型分爲普通類型和非普通類型,非普通類型包含普通的自身和非託管資源。那麼,若是類的某個字段或屬性的類型是非普通類型,那麼這個類型也應該是非普通類型,應該也要實現IDisposable接口。
舉個栗子,若是一個類型,組合了FileStream,那麼它應該實現IDisposable接口,代碼以下:
public class MyClass2 : IDisposable { ~MyClass2() { Dispose(false); } public FileStream FileStream { get; set; } /// <summary> /// 釋放標記 /// </summary> private bool disposed; /// <summary>執行與釋放或重置非託管資源關聯的應用程序定義的任務。</summary> public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// 非密封類可重寫的Dispose方法,方便子類繼承時可重寫 /// </summary> /// <param name="disposing"></param> protected virtual void Dispose(bool disposing) { if (disposed) { return; } //清理託管資源 if (disposing) { //todo } //清理非託管資源 if (FileStream != null) { FileStream.Dispose(); FileStream = null; } //告訴本身已經被釋放 disposed = true; } }
由於類型包含了FileStream類型的字段,因此它包含了非普通類型,咱們仍舊須要爲這個類型實現IDisposable接口。
可能不少人會問啊,GC已經幫咱們隱式的釋放了資源,爲何還要主動地釋放資源,咱們先來看一個例子:
private void button6_Click(object sender, EventArgs e) { var fs = new FileStream(@"C:\1.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite); } private void button7_Click(object sender, EventArgs e) { GC.Collect(); }
上面的代碼在WinForm程序中,單擊按鈕6,打開一個文件流,單擊按鈕7執行GC回收全部「代」(下文將指出代的概念)的垃圾,若是連續單擊兩次按鈕6,將會拋異常:
若是單擊按鈕6再單擊按鈕7,而後再單擊按鈕6則不會出現這個問題。
咱們來分析一下:在單擊按鈕6的時候打開一個文件,方法已經執行完畢,fs已經沒有被任何地方引用了,因此被標記爲了垃圾,那麼何時被回收呢,或者GC何時開始工做?微軟官方的解釋是,當知足如下條件之一時,GC纔會工做:
系統具備較低的物理內存;
由託管堆上已分配的對象使用的內存超出了可接受的範圍;
手動調用GC.Collect方法,但幾乎全部的狀況下,咱們都沒必要調用,由於垃圾回收器會自動調用它,但在上面的例子中,爲了體驗一下不及時回收垃圾帶來的危害,因此手動調用了GC.Collect,你們也能夠仔細體會一下運行這個方法帶來的不一樣。
GC還有個「代」的概念,一共分3代:0代、1代、2代。而這三代,至關因而三個隊列容器,第0代包含的是一些短時間生存的對象,上面的例子fs就是個短時間對象,當方法執行完後,fs就被丟到了GC的第0代,但不進行垃圾回收,只有當第0代滿了的時候,系統認爲此時知足了低內存的條件,纔會觸發垃圾回收事件。因此咱們永遠不知道fs何時被回收掉,在回收以前,它實際上已經沒有用處了,但始終佔着系統資源不放(佔着茅坑不拉屎),這對系統來講是種極大的浪費,並且這種浪費還會干擾整個系統的運行,好比咱們的例子,因爲它始終佔着資源,就致使了咱們不能再對文件進行訪問了。
不及時釋放資源還會帶來另外的一個問題,雖然以前咱們說實現IDisposable接口的類,GC能夠自動幫咱們釋放,但這個過程被延長了,由於它不是在一次回收中完成全部的清理工做,即便GC自動幫咱們釋放了,那也是先調用FileStream的終結器,在下一次的垃圾回收時纔會真正的被釋放。
瞭解到危害後,咱們在打碼過程當中,若是咱們明知道它應該被using起來時,必定要using起來:
using (var fs = new FileStream(@"C:\1.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite)) { }
在上文的內容中,咱們都提到要釋放資源,但並無說明需不須要將再也不使用的對象置爲null,而這個問題也是一直以來爭議很大的問題,有人認爲將對象置爲null能讓GC更早地發現垃圾,也有人認爲這並無什麼卵用。其實這個問題首先是從方法的內部被提起的,爲了更好的說明這個問題,咱們先來段代碼來檢驗一下:
private void button6_Click(object sender, EventArgs e) { var mc1 = new MyClass() { Name = "mc1" }; var mc2 = new MyClass() { Name = "mc2" }; mc1 = null; } private void button7_Click(object sender, EventArgs e) { GC.Collect(); } public class MyClass { public string Name { get; set; } ~MyClass() { MessageBox.Show(Name + "被銷燬了"); } }
單擊按鈕6,再單擊按鈕7,咱們發現:
沒有置爲null的mc2會先被釋放,雖然它在mc1被置爲null以後;
在CLR託管的應用程序中,有一個「根」的概念,類型的靜態字段、方法參數以及局部變量均可以被做爲「根」存在(值類型不能做爲「根」,只有引用類型才能做爲「根」)。
上面的代碼中,mc1和mc2在代碼運行過程當中分別會在內存中建立一個「根」。在垃圾回收的過程當中,GC會沿着線程棧掃描「根」(棧的特色先進後出,也就是mc2在mc1以後進棧,但mc2比mc1先出棧),檢查完畢後還會檢查全部引用類型的靜態字段的集合,當檢查到方法內存在「根」時,若是發現沒有任何一個地方引用這個局部變量的時候,無論你是否已經顯式的置爲null這都意味着「根」已經被中止,而後GC就會發現該根的引用爲空,就會被標記爲可被釋放,這也表明着mc1和mc2的內存空間能夠被釋放,因此上面的代碼mc1=null沒有任何意義(方法的參數變量也是如此)。
其實.NET的JIT編譯器是一個優化過的編譯器,因此若是咱們代碼裏面將局部變量置爲null,這樣的語句會被忽略掉:
s=null;
若是咱們的項目是在Release配置下的,上面的代碼壓根就不會被編譯到dll,正是因爲咱們上面的分析,因此不少人都會認爲將對象賦值爲null徹底沒有必要,可是,在另外一種狀況下,就徹底有必要將對象賦值爲null,那就是靜態字段或屬性,但這斌不意味着將對象賦值爲null就是將它的靜態字段賦值爲null:
private void button6_Click(object sender, EventArgs e) { var mc = new MyClass() { Name = "mc" }; } private void button7_Click(object sender, EventArgs e) { GC.Collect(); } public class MyClass { public string Name { get; set; } public static MyClass2 MyClass2 { get; set; } = new MyClass2(); ~MyClass() { //MyClass2 = null; MessageBox.Show(Name + "被銷燬了"); } } public class MyClass2 { ~MyClass2() { MessageBox.Show("MyClass2被釋放"); } }
上面的代碼運行咱們會發現,當mc被回收時,它的靜態屬性並無被GC回收,而咱們將MyClass終結器中的MyClass2=null的註釋取消,再運行,當咱們兩次點擊按鈕7的時候,屬性MyClass2才被真正的釋放,由於第一次GC的時候只是在終結器裏面將MyClass屬性置爲null,在第二次GC的時候纔看成垃圾回收了,之因此靜態變量不被釋放(即便賦值爲null也不會被編譯器優化),是由於類型的靜態字段一旦被建立,就被做爲「根」存在,基本上不參與GC,因此GC始終不會認爲它是個垃圾,而非靜態字段則不會有這樣的問題。
因此在實際工做當中,一旦咱們感受靜態變量所佔用的內存空間較大的時候,而且不會再使用,即可以將其置爲null,最典型的案例就是緩存的過時策略的實現了,將靜態變量置爲null這或許不是頗有必要,但這絕對是一個好的習慣,試想一個項目中,若是將某個靜態變量做爲全局的緩存,若是沒有作過時策略,一旦項目運行,那麼它所佔的內存空間只增不減,最終頂爆機器內存,因此,有個建議就是:儘可能地少用靜態變量。