GC兩個關鍵難點:跨代引用與併發標記

在分析JVM的分代垃圾收集算法的時候,可能存在老年代對新生代的引用,無法真正確定對象已死。


一、解決跨代引用:記憶集

記憶集(Remembered Set):一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構,在對象層面來說就是非收集區域對象對收集區域對象的引用的記錄。

它存放在收集區域,比如在新生代裡面存放著老年代對新生代對象的每一個引用。這樣在收集新生代的時候,我們就可以根據記憶集知道哪些對象被老年代對象所引用,不能回收,這就解決了跨代引用的問題。

記憶集根據記錄的精度分三類:

字長精度:記錄的是老年代指向新生代地址。

對象精度:記錄的是老年代引用的新生代對象。

卡精度:記錄的是新生代一段地址是否存在被老年代引用的記錄。


二、記憶集的實現:卡表

卡表(Card Table):是以第三種卡精度的方式實現的記憶集,也是目前最常用的方式。記憶集是抽象的概念,而卡表就是記憶集的一種具體實現。

卡表最簡單的形式可以是一個字節數組,HotSpot就是這樣實現的。

CARD_TABLE [this address >> 9] = 0;

大致示意圖

GC兩個關鍵難點:跨代引用與併發標記

把地址的值右移9位相當於除於512就是卡表索引,每字節512為一組對應卡表同一個元素,一組就是一個卡頁,如果這個卡頁中只要有一個對象被其他區域對象所引用,對應卡表元素的值就變成1,也就是所謂的元素變髒。

在垃圾回收時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁對應的內存包含跨代指針,把他們加入GC Rootsz中一併掃描。


三、卡表數據的收集:寫屏障

寫屏障:可以看成是虛擬機層面在"引用類型字段賦值"這個動作的AOP切面,引用對象賦值的時候產生一個環形通知,進行一些額外的處理,這樣就是引用對象賦值這個操作都在寫屏障的覆蓋範圍內,賦值前的寫屏障叫寫前屏障,複製後的寫屏障叫寫後屏障。

這樣我們就可以通過寫屏障,一旦發生賦值操作就可以把引用的更新寫進卡表。


四、併發的可達性標記

上一篇文章我們知道,有些垃圾收集器實現了用戶線程和收集器的標記線程併發運行的場景,但是用戶現場很可能造成引用的更改,那麼標記對象可能就不準確。

像下面這種情況:

GC兩個關鍵難點:跨代引用與併發標記

在可達性分析中我們把完成分析的標成黑色,正在分析的標成灰色,未分析的標成白色。在第一步正在分析對象B,對象C引用著對象D。這時用戶線程使對象A引用對象D,而對象C不在引用對象D。由於A已經分析過了,不會再進行分析,最終就會造成對象D沒有在引用鏈上而被回收,這樣系統就會出現異常了!

要解決這個問題,首先就要分析出現這個問題出現的原因,要同時滿足如下兩個條件:

賦值器插入了一條或多條從黑色到白色對象的新引用;

賦值前刪除了全部從灰色到該白色對象的直接或間接引用;

所以要解決這個問題也簡單,只要破壞一個條件就不會出現了。最終有兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)

1、增量更新

只要有黑色對象新加了指向白色對象的引用關係,把這個新插入的引用記錄下來,等併發掃描結束後,在將這個黑色對象為根重新掃描。

增量更新破壞了第一個條件,使新加的對象都能重新回到引用鏈上。

2、原始快照

當灰色對象刪除指向白色對象的引用關係時,就記錄這個將要刪除的引用,併發掃描結束後,在以白色對象為根重新掃描一次。

原始快照破壞的是第二個條件,也就是說不管引用關係是否刪除,都會按照掃描那一刻的對象圖快照進行搜索。這樣就保證了對象一定能在調用鏈上,不過會有少量確實應該回收的存在。


CMS用的增量更新,G1則是原始快照.


五、總結

記憶集記錄著收集區域被其他區域引用的數據(地址,對象,內存段)。

卡表是記憶集的具體實現。卡表可以是一個簡單的字節數組結構,數組的索引表示的是收集區域的一段內存區域,而元素的值就代表著對應的地址上是否被其他區域所引用。

JVM通過實現AOP切面對"引用對象賦值"操作進行監聽,實現把跨代引用記錄到記憶集中。

併發的可達性標記可能造成存活對象丟失。丟失的條件是對象從還沒有被分析完成對象的引用中移除,然後又被加入到已經完成分析對象的引用中,導致對象沒有標記在引用鏈上而被回收。解決的方案分別是增量更新(破壞第一條)、原始快照(破壞第二條)。

CMS收集器採用的是增量更新,G1則是採用的原始快照。


Java程序員日常學習筆記,如理解有誤歡迎各位交流討論!

GC兩個關鍵難點:跨代引用與併發標記


分享到:


相關文章: