本文內容是學習CLR.via C#的21章后個人整理,有不足之處歡迎指導。
昨天是1024,coder的節日,我為自己coder之路定下一句准則--保持學習,保持自信,保持謙遜,保持分享,越走越遠。
第一部分—基本原理思想
垃圾回收機制是針對托管堆而言。
不同於C的運行時堆,托管堆是內存是連續的,每次分配新內存,NextObjPtr指針只需要加上新分配內存塊大小即可。C運行時堆為了維護鏈表的完整性,每當分配新的內存時,遍歷鏈表,一旦發現足夠大的內存塊,則拆分塊,修改節點中的指針。從托管堆中分配內存的速度,幾乎可以與線程棧分配相媲美。
GC機制回收的就是托管堆中的垃圾對象。
第二部分—基本算法思想
GC檢查托管堆中是否有不再使用的對象。
那么什么是不再使用的對象?
首先要解釋什么是根(root)。每個應用程序都有一組根,每個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要么運用托管堆中的一個對象,要么為null。類型中定義的任何靜態字段被認為是一個根,任何方法參數或者局部變量也被認為是一個根。只有引用類型變量,才被認為是根,值類型的變量永遠不被認為是根。
GC開始執行時,假設所有對象都是垃圾。
GC沿着線程棧上檢查所有的根,如果發現了一個跟引用堆中的對象,則在這個對象的“同步索引字段”上開啟一位,也就是將這個bit設置為1,也就是說這個對象被標記了。
GC就是這樣,以遞歸的方式遍歷所有可達的對象。可達的對象也就是說有根的對象,就是在標記階段被標記的,也就是本次不回收的。所以不可達的對象就被回收了。
進入第二階段--壓縮階段。
實際上此壓縮非彼壓縮,在這里是指碎片整理,如何整理呢?如果發現曉得內存塊,GC忽略它們,如果發現大的,可用的連續內存塊,GC把非垃圾對象移動到這里以壓縮堆。
包含只想這些對象的指針的變量和CPU寄存器,現在都會變得無效,NextObjPtr也應重新指向托管堆的結尾。
第三部分—終結列表和F-reachable隊列
說到終結列表要從某些不僅占用內存的對象說起,比如FileStream,它不僅占用內存資源,也在占用本地資源。
Finalize方法就是用於釋放本地資源的方法。
那么這個終結列表用來做什么呢?微軟當然不會畫蛇添足,請仔細看好下面這段:
既占用內存資源,又占用本地資源的,在GC回收這樣對象所占的內存時,僅僅回收內存時不夠的,因為一定要調用Finalize方法來釋放本地資源啊!強烈不建議在代碼中我們手動Finalize,這需要堆Finalize的實現有相當深刻並且全面的理解。那么微軟的GC是什么怎么來給我們執行的呢?這就用到了終結列表,為了一定要保證執行Finalize,在最初我們new操作符分配內存地時候,如果該對象的類型中定義了Finalize方法,那么將該對象的一個指針方法到終結列表當中,當此類對象在托管堆中判定為垃圾的時候,GC掃描終結列表,以查找這些對象的指針,該指針會從終結列表中移除,並追加到F-reachable隊列。
那么這個隊列干嘛的呢?我認為唯一的目的就是復活對象,並調用Finalize方法釋放本地資源(如果對象是死的,我們無法調用其方法),調用方法的是一個微軟定義好的優先級比較高的一個線程,聽說這樣做有很多好處。那么為什么放到F-reachable隊列中就復活了?f(finalization)終結,reachable可達的。換言之,可將這個隊列看做靜態字段那樣的一個跟。
第四部分—代的思想和原理
GC機制無論何時,都分為三代,0代,1代,2代。
代是什么,微軟關於這個做了假設,新對象生存周期比較短,而老對象傾向於活的久一些。所在代越高的對象,存活的越久,所在代越低的對象,越容易被回收。
代就是托管堆中被分配的內存而已,也可以說把托管堆分成三部分吧?
0代初始的預算大小為256kb,當0代中的內存用完時,為新對象分配內存,0代內存不夠用時,GC開始回收第一代,未被標記的對象當然回收,已標記的對象則這些對象提高一代,進入1代區域,與此同理,當一代內存已滿時,回收1代,此時根對象也就是標記的對象提升至2代。
再次重點說一下三代預算大小分別為256KB,2MB,10MB,預算大小以提升性能為宜,預算越大,垃圾回收頻率越低。再次注意的是,性能提升的理論源於開始的假設:新對象生存期較短,老對象傾向於活的久一些。請仔細看下面一段。
如果GC回收后,0代存活下來的對象很少,或者說回收的內存很多。0代預算可能會從256調整為128。代的分配空間減少,意味着回收頻繁回收頻繁,但GC所做的工作會減少,從而減小進程的工作集,最理想的狀態是0代對象都是當做垃圾被回收,這樣不必壓縮內存,NextObjPtr指向0代起始處。這樣來講,最開始的假設是是成立的。