圖解JVM如何解決對象跨代引用的問題
在新生代做GCRoots可達性掃描過程中,可能會碰到跨代引用的對象的問題,如下圖所示:
圖片
在新生代掃面的時候會掃描不到D對象,如果這個D對象不去老年代掃描是否有被引用,那么就會無法觸達,如下如所示:
圖片
但是D對象是有被引用的,如果直接回收D對象就會造成一些意想不到的問題,如果為了掃描D對象是否存在跨代引用而對老年代整體掃描一遍,就會帶來整個垃圾回收的效率低下的問題。
為了解決跨代引用的問題,可以在新生代可以引入RememberSet(記錄從非收集區到收集區的指針集合)的數據結構,稱為記憶集,這樣避免把整個老年代加入到GCRoots掃描范圍中,如下所示:
圖片
記憶集是一種概念,在hotspot使用名為“卡表”(Cardtable)的方式實現記憶集,它也是目前最常用的一種方式。卡表使用一個字節數組來實現:CARD_TABLE[],每個元素對應著其標識的內存區域一塊特定大小的內存塊,我們稱之為“卡頁”,如下圖所示:
圖片
將老年代按照一個卡頁大小(512字節)分成n個區域,這個區域的地址信息都記錄到卡表中。當某個區域中出現跨代引用的時候,我們就在卡表中記錄信息,如下所示:
圖片
老年代的F對象引用了年輕代的D對象,那么我們就在卡表中記錄a卡頁中有跨代引用(設置對應區域的值為1,如上a=1;卡頁b沒有跨代引用,設置b=0來表示此區域無跨代引用)。
當年輕代做GCRoots掃描的時候,我們去卡表中查詢哪些區域存在跨代的對象,然后判斷這個對象是否還繼續存活,如下所示:
圖片
一個卡頁中可包含多個對象,只要有一個對象存在跨代引用,那么其對應在卡表中的元素標識就設置成1,表示該區域存在跨代引用,否則為0。GC時只要篩選本收集區的卡表中為1區域中的元素加入GCRoots里(如上圖中a=1就被篩選出來做GCRoots)。
通過卡表的方式我們就避免了大規模的掃描老年代對象,假設老年代有上萬的對象存活,年輕代之后幾十個存活對象,通過卡表我們只需要掃面少部分的老年代對象,大大的提升垃圾收集的效率。
在hotspot使用寫屏障維護卡表中數據狀態,當老年代引用了新生代的對象時候,底層會維護將卡表中的對應的區域設置為1。