JVM(十二)記憶集和卡表


G1及其后出現的垃圾收集器ZGC、Shenandoah,它們都是基於Region的內存布局形式。它們垃圾收集的目標范圍不再是整個新生代(Minor GC)、老年代(Majon GV)、整個堆(Full GC),而是一個一個的Region。因為這樣的內存布局,所以G1能做到面向局部收集。

每個Region都可以被標記為E(Eden)、S(Survivor)、O(Old)、H(Humongous),但一個Region同一時刻只能是這四個中的一個。H表示巨型對象,即超過Region大小的一半的對象,會直接進入老年代由多個連續的Region存儲。

Region的大小可以通過-XX:G1HeapRegionSize參數指定,如果沒有顯示指定,則G1會計算出一個合理的大小。Region的取值范圍為1M~32M,且應為2的N次冪,所以Region的大小只能是1M、2M、4M、8M、16M、32M。比如-Xmx=16g -Xms=16g,則Region的大小等於16G / 2048=8M。也可以推理出G1推薦的管理的最大堆內存是64G

RSet(Remembered Set、記憶集)

在垃圾收集過程中,會存在一種現象,即跨代引用,在G1中,又叫跨Region引用。如果是年輕代指向老年代的引用我們不用關心,因為即使Minor GC把年輕代的對象清理掉了,程序依然能正常運行,而且隨着引用鏈的斷掉,無法被標記到的老年代對象會被后續的Major GC回收。如果是老年代指向年輕代的引用,那這個引用在Minor GC階段是不能被回收掉的,那如何解決這個問題呢?

最簡單的實現方式當然是每個對象中記錄這個跨Region引用記錄,GC時掃描所有老年代的對象,顯然這是一個相當大的Overhead。為什么呢?因為IBM做過這樣的實驗,發現絕大多數對象都是“朝生夕滅”,等不到進入老年代,能進入老年代的對象最多不到5%。JVM的新生代內存比例是8:1:1也是基於這個結論設定的。

最合理的實現方式自然是記錄哪些Region中的老年代的對象有指向年輕代的引用。GC時掃描這些Region就行了。這就是RSet存在的意義。RSet本質上是一種哈希表,Key是Region的起始地址,Value是一個集合,里面存儲的元素是卡表的索引號(第幾個Card的第幾個元素)。

Card Table(卡表)

每個Region又被分成了若干個大小為512字節的Card,這些Card都會記錄在全局卡表中。Card中的每個元素對應着其標識的內存區域中一塊特定大小的內存塊,這個內存塊被稱為卡頁。一個卡頁的內存中通常不止一個對象,只有卡頁中有一個及以上對象的字段存在着跨Region引用,這個對應的元素的值就標識為1。

比如G1默認的Region有2048個,默認每個Region為2M,那每個Region對應的Card的每個元素對應的卡頁的大小為2M / 512=4K,即這4K內存中只要有一個或一個以上的對象存在着跨Region對年輕代的引用,這個卡頁對應的Card的元素值為1。

 

 這樣在Minor GC時,只需要將變臟的Region中的那個卡頁加入GC Roots一並掃描即可。比起掃描老年代的所有對象,大大減少了掃描的數據量,提升了效率。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM