.NET的垃圾回收機制是一個非常強大的功能,盡管我們很少主動使用,但它一直在默默的在后台運行,我們仍需要意識到它的存在,了解它,做出更高效的.NET應用程序;下面我分享一下我對於垃圾回收機制(GC)的學習心得。
GC的必要性
我們知道程序會需要向內存堆使用new請求內存,然后將請求的內存初始化並使用,使用完畢之后,變清理資源和釋放內存,等待別的程序來請求使用;對內存資源的管理方式,現在存在這么幾種管理方式:
1、手動管理:C、C++
2、計數管理:COM
3、自動管理:.NET、JAVA、PHP
現在的高級語言基本上都實現了自動管理內存,這是因為手動管理內存會因為人為的原因產生以下問題:
1、開發人員忘記釋放請求的內存,造成內存泄漏,若是內存泄露過多,則可能會造成內存溢出,導致程序無法運行;
2、應用程序訪問已釋放的內存,造成數據讀取錯誤。
由此可見,手動去管理堆里面的內存可靠程度,會因開發人員的不同而不同,在C++因指針而出現的問題可不少;而且易出現Bug等亂七八糟的問題,影響系統穩定性,所以自動化管理內存是必要的。
GC的工作原理
通用概念
回收時機
當應用程序分配新的對象,GC的代的預算大小已經達到閾值,比如GC的第0代已滿;
代碼主動顯式調用System.GC.Collect();
其他特殊情況,比如,windows報告內存不足、CLR卸載AppDomain、CLR關閉,甚至某些極端情況下系統參數設置改變也可能導致GC回收。
應用程序根
應用程序根(application root):根(root)就是一個存儲位置其中保存着對托管堆上一個對象的引用,根可以屬性下面任何一個類別
- 全局對象和靜態對象的引用
- 應用程序代碼庫中局部對象的引用
- 傳遞進一個方法的對象參數的引用
- 等待被終結(finalize,后面介紹)對象的引用
- 任何引用對象的CPU寄存器
代
垃圾回收器將托管堆(heap)里面的對象划分為3個代(一般為3代),可以使用GC.MaxGeneration()方法來進行查詢當前系統所支持的最大代數:
1、G0 小對象(Size<85000Byte):新分配的小於85000字節的對象
2、G1:在GC中幸存下來的G0對象
3、G2:大對象(Size>=85000Byte);在GC中幸存下來的G1對象
當一個對象被new的時候,它的代為0,經過一次回收之后,若該對象沒有被回收,則代上升,變為1,若每次回收都幸存下來,則代都會上升,最大代為操作系統所支持的最大代。
因為將對象以代划分,並且可以單獨回收某一個世代,避免回收整個托管堆,提升性能。一個基於代的垃圾回收器有一下特點:
1、對象越新,生存期越短;
2、對象越老,生存期越長;
3、回收堆的一部分,速度快於回收整個堆。
工作過程
標記對象
在垃圾回收的第一步就是標記對象:垃圾回收器會認為托管堆中的所有對象都是垃圾,然后垃圾回收器會去檢查所有的應用程序根,遍歷每個根所引用到的對象,將其標記為活動的(live ),所有的根對象都檢查完之后,有標記的對象就是可達對象,未標記的對象就是不可達對象,不可達對象就是回收的目標。
弱引用對象則不在考慮范圍之內,所以一定會被回收掉的。
銷毀對象,釋放內存
在經過第一步的對象篩選之后,回收沒有被引用的對象,就是不可達對象,GC調用對象默認的終結器Finalize(),銷毀對象之后,將內存也釋放掉。
同時,還存在引用的對象,就是可達對象的世代變為下一個世代。
壓縮堆內存
經過第二步的銷毀對象和釋放內存之后,幸存下來的對象在堆中的排列可能是不連續的,這時在堆中存在非常多的內存碎片,程序在new對象的時候都是請求一段連續的內存,則內存碎片可能就無法再次利用(雖然沒有被使用),造成內存資源的浪費,所以垃圾回收的最后一步就是壓縮內存:將垃圾回收后幸存的對象移動到一起,並且將各個對象的引用更新到對象新的位置上,保證對象引用的正確性。
注:從這里看得出,在壓縮堆內存的時候,所有相關線程必須暫停,因為壓縮時不能保證對象引用的正確性,所以在垃圾回收的時候,GC會劫持所有相關線程,在回收完畢之后,被劫持的線程才會正常工作,所以垃圾回收勢必會影響一定的性能,所以慎用System.GC.Collect()。
Finalize()與Dispose()
上面說到,GC在回收對象的時候是調用對象的終結器Finalize()來實現的,那么,就簡單的總結一下Finalize()與Dispose()吧:
1、調用者:
Finalize只能由GC調用
Dispose由開發人員顯示調用,也可以使用use區塊,在程序離開區塊使自動調用Dispose方法
2、調用時機:
Finalize由於是GC調用的,所以調用時機是垃圾回收的時候調用,時機不確定
Dispose由於是顯示調用,所以調用時機是確定的,在調用方法的時候就調用了
3、目的:
這里的目的主要說是Dispose出現的目的;
首先是.NET存在托管資源和非托管資源,一般來說,非托管資源數量有限,比較珍貴,在使用完畢之后,希望能夠釋放掉,那么將釋放非托管資源的方法寫到終結器Finalize里面也是可以的,但是由於Finalize的調用時機不確定,導致釋放資源不及時,那么有限的非托管資源很快就被占用完畢,所以,為了能夠及時的釋放掉這類資源,我們需要能夠顯示調用的方法,這就是Dispose。
Finalize主要是為了GC釋放托管資源和銷毀對象,釋放內存
Dispose主要是為了釋放托管和非托管資源和銷毀對象,釋放內存
注:不必擔心資源的重復釋放問題,就算是重復釋放,.NET也做好了相應措施來處理,不會拋出異常。
下面貼一個MSDN推薦的標准的Dispose實現方式

class Class : IDisposable { // 標識:是否釋放托管資源 private bool disposed = false; // 顯示調用的方法 public void Dispose() { Dispose(true); // 將對象從垃圾回收器鏈表中移除, // 從而在垃圾回收器工作時,只釋放托管資源,而不執行此對象的析構函數 GC.SuppressFinalize(this); } // 受保護的釋放資源方法 protected virtual void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { // 此處寫釋放托管資源的方法 } disposed = true; // 此處寫釋放非托管資源的方法 } } ~Class() { // 這里是防止忘記顯示調用Dispose(),在GC進行垃圾回收的時候進行釋放非托管資源 Dispose(false); } }
總結
GC所帶來的便利是不言而喻的,但是這是付出一定的系統性能來實現的:在垃圾回收的時候GC會劫持所有相關的線程,並且會有一定的時空開銷,所以在平時開發過程中注意一些良好的開發習慣可能會對GC有一些積極的影響。
1、盡量不要new很大的對象,大對象(>=85000Byte)直接歸為G2代,GC回收算法從來不對大對象堆(LOH)進行內存壓縮整理,移動大對象將會消耗更多的CPU時間,也更容易造成內存碎片。這里也可以將大對象或者生命周期長的對象進行池化。
2、不要頻繁的new生命周期短的小對象,這可能會導致頻繁的垃圾回收,這里可以考慮使用結構體放在棧中來代替,或者也可以使用對象池化來優化。
3、不推薦使用對象池化的解決方案,它比較笨重和容易出錯,設計一個高性能穩定的對象池並不容易。
4、降低對象之間的縱向深度,GC在回收過程中,會先順着根來進行對象遍歷和標記,減少深度可以加快遍歷速度;若系統中各個類之間的關系錯綜復雜,那么考慮一下設計方案是否合理。
當然注意的地方還有不少,最后貼一篇博客,這里介紹了如何編寫高性能的.NET代碼,其中的GC介紹非常詳細: