在本系列的第一篇文章《C#堆棧對比(Part Three)》中,介紹了值類型和引用類型在Copy上的區別以及如何實現引用類型的克隆以及使用ICloneable接口等內容。
本文為文章的第四部分,主要講解內存回收原理與注意事項,以及如何提高GC效率等問題。
注:限于本人英文理解能力,以及技術經驗,文中如有錯誤之處,還請各位不吝指出。
C#堆棧對比(Part One)
C#堆棧對比(Part Two)
C#堆棧對比(Part Three)
C#堆棧對比(Part Four)
讓我們從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 }}在對象創建期間,所有帶有終結器的對象被加入到了終結隊列。我們假設Object1、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 =newResourceUser())這樣的模式更符合實際需要和存在的價值。
注:這里作者強調的是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); } C# 4之前設想一下這種情況:如果第一個線程在執行完Enter(tmp)之后意外退出,也就是沒有執行Exit(tmp),則第二個線程將永遠阻塞在Enter這里等待其他人釋放資源,這就是一個典型的死鎖案例。
在C#4以及之后的Framework中增加了對Monitor.Enter的重載,將會為我們在一定程度上解決可能發生的死鎖問題:

新聞熱點
疑難解答