.NET的垃圾回收機制是一個非常強大的功能,儘管我們很少主動使用,但它一直在默默的在後臺運行,我們仍需要意識到它的存在,瞭解它,做出更高效的.NET應用程式;下麵我分享一下我對於垃圾回收機制(GC)的學習心得。 GC的必要性 我們知道程式會需要向記憶體堆使用new請求記憶體,然後將請求的記憶體初始化並使用 ...
.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); } }View Code
總結
GC所帶來的便利是不言而喻的,但是這是付出一定的系統性能來實現的:在垃圾回收的時候GC會劫持所有相關的線程,並且會有一定的時空開銷,所以在平時開發過程中註意一些良好的開發習慣可能會對GC有一些積極的影響。
1、儘量不要new很大的對象,大對象(>=85000Byte)直接歸為G2代,GC回收演算法從來不對大對象堆(LOH)進行記憶體壓縮整理,移動大對象將會消耗更多的CPU時間,也更容易造成記憶體碎片。這裡也可以將大對象或者生命周期長的對象進行池化。
2、不要頻繁的new生命周期短的小對象,這可能會導致頻繁的垃圾回收,這裡可以考慮使用結構體放在棧中來代替,或者也可以使用對象池化來優化。
3、不推薦使用對象池化的解決方案,它比較笨重和容易出錯,設計一個高性能穩定的對象池並不容易。
4、降低對象之間的縱向深度,GC在回收過程中,會先順著根來進行對象遍歷和標記,減少深度可以加快遍歷速度;若系統中各個類之間的關係錯綜複雜,那麼考慮一下設計方案是否合理。
當然註意的地方還有不少,最後貼一篇博客,這裡介紹了如何編寫高性能的.NET代碼,其中的GC介紹非常詳細: