在本系列的第一篇文章《C#堆棧對比(Part Three)》中,介紹了值類型和引用類型在Copy上的區別以及如何實現引用類型的克隆以及使用ICloneable接口等內容。html
本文爲文章的第四部分,主要講解內存回收原理與注意事項,以及如何提升GC效率等問題。數據庫
注:限於本人英文理解能力,以及技術經驗,文中若有錯誤之處,還請各位不吝指出。小程序
C#堆棧對比(Part Four)ide
讓咱們從GC的角度來看一看。若是咱們負責「倒垃圾」(taking out the trash),咱們須要高效率的作這件事。顯然,咱們要判斷什麼東西是垃圾,什麼東西不是(這對那些什麼東西都不捨得仍的人會有一些麻煩)。函數
爲了決定留下哪些東西,咱們首先假設在垃圾箱中的都是沒有用的東西(如角落裏的報紙、閣樓裏的垃圾箱、廁所裏的全部東西等等)。想象一下,咱們正在和兩個「朋友」生活在一塊兒:約瑟夫-伊凡-托馬斯(Joseph Ivan Thomas, JIT)和辛迪-洛林-里士滿(Cindy Lorraine Richmond,CLR)。約瑟夫和辛迪記錄着內存的使用以及給咱們反饋記錄信息。咱們將最開始的反饋信息列表稱做「根」列表,由於咱們將從用它開始。咱們將保持一個主列表去描繪一個圖形,這個圖形顯示了在房間中每同樣東西的位置。任何咱們須要的使事物能工做的東西咱們都將增長進這個列表中(就像咱們看電視的時候不會把遙控器放的很遠,咱們玩電腦的時候就會把鍵盤和顯示器放在「列表」中)。工具
注:做者文章中的JIT和CLR只是以首字母人名的方式來簡稱概念名稱。這一段文章的意思是引出一個概念:性能
這也是GC如何決定回收與否的原理。GC收到從JIT編譯器和CLR根列表之中的對象引用,而後遞歸地查找對象引用,這樣就能建立一個圖來描述那些對象咱們應該保存。
根列表的組成:
● 全局/靜態指針。在靜態變量中這是一個經過保持引用的方式來確保咱們的對象不被回收。
● 指針是在棧(線程棧)上的。咱們不想將線程還須要繼續執行的任何東西扔掉。
● CPU註冊的指針。任何一個CPU指向一個內存地址的在託管堆上的指針都將被保護(不要丟掉這些指針)。
上圖中,Object1,Objetc3和Object5在託管堆中是被根列表所引用的,Object1和Object5是被直接引用(指針指向)的,而Object3是在遞歸搜索中發現的。若是咱們將這個例子和電視機遙控器例子來作對比的話,就會發現Object1是電視機,Object3是遙控器。當這些都被圖形化(圖形化顯示引用關係)後,咱們將進行下一步,壓制操做(compacting)。
如今,咱們已經繪製出了咱們須要保留的對象的圖形,咱們能把「保留的對象」放在一塊兒。
注:灰色Box是沒有被引用的對象,咱們能夠移除,而且從新整理託管堆,使還保持引用的對象能「挨的近一些」,以便保持託管堆空間整齊。
幸運的是,在生活中咱們可能在咱們放其餘一些東西的時候不須要整理屋子。因爲Object2沒有被引用,GC將向下移動Object3而且修復Object1的指針。
注:這裏說的「修復Object3」的指針是由於Object3移動以後其內存地址改變了,因此也同時要更新指向Object3的指針地址,原理上來說指針僅僅知道一個地址值而不知道哪一個是Object3.
做者在原文中沒提到的是GRAPH指向Object的指針也須要更新地址值,固然這不是主要關注點,以上爲我的觀點。
下一步,GC將Object5向下移動,以下圖:
如今咱們已經整理好了託管堆,咱們僅僅須要一個便條而後放置在咱們剛剛壓制好的託管堆的頂部來讓Claire(實際上是Cindy彷佛做者總記錯女友的名字J,CLR)知道在應該在那裏放置新對象,以下圖所示:
瞭解GC的本質能幫助咱們更好的理解可能很是低效的內存對象移動的狀況。正如你所見到的,若是咱們下降咱們須要移動的對象的大小那將是有意義的,因爲產生了更小的對象拷貝,這將總體上爲GC提升很大的工做效率。
注:這裏可能涉及的意義在於LOH大對象堆在內存中的管理問題,通常來講,依據咱們的業務場景來「設計」內存數據分佈,進而更好的管理大對象和一些常常要被建立和刪除的內存碎片對象。第二個好處是幫助咱們理解GC是如何回收垃圾數據的,回收以後又有哪些操做,這些操做有什麼樣的影響,以及GC是如何依據「代」來管理垃圾的等等。
做爲一個負責回收垃圾的的人,一個問題是當咱們清理屋子的時候,汽車中的東西該怎麼處理。假設的前提是咱們清理東西的時候,咱們每樣東西都要清理的。也就是說若是筆記本在屋子裏,電池在汽車中該如何處理?
注:依據上下文的理解來看,做者想表達的意思是屋子裏的垃圾天然遲早都會扔掉(託管資源),汽車裏的垃圾大多數狀況下可能因爲開車人的疏忽而沒有扔掉(相似於非託管資源),而咱們又是一個追去完美的人,必須清楚掉全部垃圾(包括汽車裏的),那該怎麼作呢?
現實的狀況是GC須要執行代碼去清理非託管資源,如文件句柄、數據庫鏈接、網絡鏈接等等。處理這些一個極可能的方式是利用終結函數(finalizer被稱做析構器,這裏借用C++的表達方式,其實本質是同樣的 )。
注:析構函數不只僅在C++中可用,在C#代碼中仍然可用,只是在更多的時候咱們會在代碼中繼承並實現IDisposeable接口去讓GC調用Dispose()方法回收資源(更多請參考標準Dispose模式),終結器是在Dispose以後執行的而且確保當調用者沒有調用Dispose的狀況下也執行類的垃圾回收,不少時候使用Using(var a = new Class())語法糖的時候程序會自動執行Class的Dispose 方法,若是沒有調用Dispose方法並且還存在非託管資源,這將會致使內存泄漏(Memory Leak)。
class Sample { ~Sample() { // FINALIZER: CLEAN UP HERE } }
在對象建立期間,全部帶有終結器的對象被加入到了終結隊列。咱們假設Object一、Object4和Object5帶有終結函數而且在終結隊列中。讓咱們看看發生了什麼,當對象Object2和Object4再也不被程序所引用時,他們已經爲垃圾回收準備好了,以下圖:
對象Object2按照正常的方式回收。然而,當咱們回收對象Object4時,GC知道它在終結隊列中而且代替直接回收資源而將Object4(指針)移動到一個新的名叫Freachable的隊列中。
專門的線程會管理Freachable隊列,當Object4終結器被執行時,它將被從Freachable隊列中移除,這樣Object4才準備好被回收,以下圖:
因此,Object4將在下次GC回收時被回收掉。
由於在類中增長終結器會給GC增長額外的工做,因此這將是一個很昂貴的操做而且給垃圾回收增長負面性能上的影響。當你肯定須要這樣作時才能使用終結器,不然要十分謹慎。
能夠確定的作法是回收非託管資源。正如你所想的,最好是明確額關閉鏈接,而且使用IDisposeable接口代替手動編寫終結器。
實現IDisposeable接口的類會有一個清理方法Dispose()(這個方法是IDisposeable接口惟一干的一件事)。因此咱們用這個接口代替終結器:
public class ResourceUser { ~ResourceUser() // THIS IS A FINALIZER { // DO CLEANUP HERE } }
用IDisposable接口重構以後的代碼,以下:
public class ResourceUser : IDisposable { #region IDisposable Members public void Dispose() { // CLEAN UP HERE!!! } #endregion }
IDisposeable接口被集成進了Using關鍵字(語法糖),在Using結束時Dispose方法被調用。在Using內部的對象將失去做用域,由於本質上它被認爲已消失(回收)而且等待GC回收。
public static void DoSomething() { ResourceUser rec = new ResourceUser(); using (rec) { // DO SOMETHING } // DISPOSE CALLED HERE // DON'T ACCESS rec HERE }
我喜歡使用Using語法糖,由於從直觀感受上更有意義而且rec臨時變量在using塊的外部沒有存在的意義。因此,using (ResourceUser rec = new ResourceUser())這樣的模式更符合實際須要和存在的價值。
注:這裏做者強調的是rec變量的做用域問題,若是隻在Using塊內部則須要在Using後的括號內生命。
經過Using使用實現了IDisposeable接口的類,這樣咱們就能代替那些須要編寫終結器產生GC耗能的方式。
class Counter { private static int s_Number = 0; public static int GetNextNumber() { int newNumber = s_Number; // DO SOME STUFF s_Number = newNumber + 1; return newNumber; } }
若是兩個線程同時調用GetNextNumber方法而且都在S_Number增長前,他們將返回相同的結果!只有一種方式能保證結果符合預期,就是同時只有一個線程能進入到代碼中。做爲一個最佳實踐,你將盡量的Lock住一段小程序,由於線程不可不在隊列中等待Lock住的方法執行完畢,即便多是低效的。
class Counter { private static int s_Number = 0; public static int GetNextNumber() { lock (typeof(Counter)) { int newNumber = s_Number; // DO SOME STUFF newNumber += 1; s_Number = newNumber; return newNumber; } } }
注:1. Lock本質是線程信號量的鎖定方式,在原文中有人對lock(typeof(Counter))指出了質疑,雖然做者並未回覆,但做者確實犯了這個錯誤,「咱們永遠不要鎖住類型Typeof(Anything)或者是lock(this)」,用private readonly static object syncLock = new Object(); lock(syncLock){…}這種方式,這裏只說結論不作代碼演示,各位若是想了解的話可網上搜索一下。
2. 這裏有一個細節要說一下,在C#4以前的代碼lock極可能會編譯成:
object tmp = listLock; System.Threading.Monitor.Enter(tmp); try { // TODO: Do something stuff. System.Threading.Thread.Sleep(1000); } finally { System.Threading.Monitor.Exit(tmp); }
設想一下這種狀況:若是第一個線程在執行完Enter(tmp)以後意外退出,也就是沒有執行Exit(tmp),則第二個線程將永遠阻塞在Enter這裏等待其餘人釋放資源,這就是一個典型的死鎖案例。
在C#4以及以後的Framework中增長了對Monitor.Enter的重載,將會爲咱們在必定程度上解決可能發生的死鎖問題:
bool acquired = false; object tmp = listLock; try { #region Description //// //// Summary: //// Attempts to acquire an exclusive lock on the specified object, and atomically //// sets a value that indicates whether the lock was taken. //// //// Parameters: //// obj: //// The object on which to acquire the lock. //// //// lockTaken: //// The result of the attempt to acquire the lock, passed by reference. The input //// must be false. The output is true if the lock is acquired; otherwise, the //// output is false. The output is set even if an exception occurs during the //// attempt to acquire the lock. //// //// Exceptions: //// System.ArgumentException: //// The input to lockTaken is true. //// //// System.ArgumentNullException: //// The obj parameter is null. //[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] //public static void TryEnter(object obj, ref bool lockTaken); #endregion System.Threading.Monitor.Enter(tmp,ref acquired); // TODO: Do something stuff. System.Threading.Thread.Sleep(1000); } finally { if (acquired) { System.Threading.Monitor.Exit(tmp); } }
——以上內容出自《深刻理解C# Edition2》
咱們第二個要注意的地方是靜態變量的引用。記住,被「根」列表索引的對象沒有被回收。這裏舉一個最醜陋的例子:
class Olympics { public static Collection<Runner> TryoutRunners; } class Runner { private string _fileName; private FileStream _fStream; public void GetStats() { FileInfo fInfo = new FileInfo(_fileName); _fStream = _fileName.OpenRead(); } }
因爲Olympics類中的Runner集合是靜態的,因此它沒有被GC釋放(還被「根」列表所引用),可是你可能也注意到了,當咱們每次調用GetStats方法時,它打開了一個文件。又因爲它沒有被關閉也沒有被回收,咱們將面臨一個大災難。想象一下咱們有10萬個Runner報名參加奧林匹克。咱們將以許多不可回收的對象而結束。Ouch!咱們在談論的是低性能問題!
一個保持對象更節省資源的方式是隻保持一個對象在應用程序全局。咱們將用GoF的單例模式。
經過「工具類」(靜態的Utility類 or XXXHelper類)在內存中保持一個單例是節省資源的小把戲。最佳實踐是單例模式。咱們要當心的用靜態變量,由於他們真的是「全局變量」而且致使咱們頭疼和遇到不少奇奇怪怪的行爲在改變線程狀態的多線程程序中。若是咱們使用單例模式,咱們應該想清楚。
public class SingltonPattern { private static Earth _instance = new Earth(); private SingltonPattern() { } public static Earth GetInstance() { return _instance; } }
咱們定義了私有類型的構造函數因此SingltonPattern類不能在外部被實例化。咱們只能經過靜態方法GetInstance獲取實例。這將是線程安全的,由於CLR對靜態變量的保護。這個例子是我所見到的單例模式中最優雅的方式。
注:原文讀者有人質疑過此單例模式。在這裏做者的單例模式更符合單例的原則。
咱們總結一下能提升GC效率的方法:
1. 清理乾淨。不要保持資源一直開啓!肯定的關閉全部已打開的鏈接,儘量的清理全部非託管對象。當使用非託管資源時一個原則是:儘量晚的初始化對象而且儘快釋放掉資源。
2. 不要過分的使用引用。合理的利用引用對象。記住,若是咱們的對象還存活,咱們應該將對象設置爲null。我在設置空值時的一個技巧是使用NullObject模式來避免空引用帶來的異常。當GC開始回收時越少的引用對象存在,越有利於性能。
3. 簡單的用終結器。對GC來講終結器十分消耗資源,咱們只有在十分肯定的方式下使用終結器。若是咱們能用IDisposeable代替終結器,它將十分有效率,由於咱們的GC一次就能回收掉資源,而不是兩次。
4. 將對象和其子對象放在一塊兒。便於GC拷貝大數據而不是數據碎片,當咱們聲明一個對象時,儘量將內部全部對象聲明的近一些。
2016-01-04 更新垃圾回收圖片,多謝@basonson指出圖片錯誤。