當前主流編程語言的垃圾收集器基本上都是依靠可達性分析演算法來判定物件是否存活的,可達性分析演算法理論上要求全程序都基于一個能保障一致性的快照中才能夠進行分析,這意味著必須全程凍結用戶執行緒的運行,
- 在根節點列舉這個步驟中,由于 GC Roots 相比起整個 Java 堆中全部的物件畢竟還算是極少數,且在各種優化技巧(如 OopMap)的加持下,它帶來的停頓已經是非常短暫且相對固定(不隨堆容量而增長) 的了,
- 可從 GC Roots 再繼續往下遍歷物件圖,這一步驟的停頓時間就必定會與 Java 堆容量直接成正比例關系了:堆越大,存盤的物件越多,物件圖結構越復雜,要標記更多物件而產生的停頓時間自然就更長,
要知道包含 “標記” 階段是所有追蹤式垃圾收集演算法的共同特征,如果這個階段會隨著堆變大而等比例增加停頓時間,其影響就會波及幾乎所有的垃圾收集器,同理可知,如果能夠削減這部分停頓時間的話,那收益也將會是系統性的,
想解決或者降低用戶執行緒的停頓,就要先搞清楚為什么必須在一個能保障一致性的快照上才能進行物件圖的遍歷?為了能解釋清楚這個問題,我們引入三色標記(Tri-color Marking)作為工具來輔助推導,把遍歷物件圖程序中遇到的物件,按照 “是否訪問過” 這個條件標記成以下三種顏色:
- 白色:表示物件尚未被垃圾收集器訪問過,顯然在可達性分析剛剛開始的階段,所有的物件都是白色的,若在分析結束的階段,仍然是白色的物件,即代表不可達,
- 黑色:表示物件已經被垃圾收集器訪問過,且這個物件的所有參考都已經掃描過,黑色的物件代表已經掃描過,它是安全存活的,如果有其他物件參考指向了黑色物件,無須重新掃描一遍,黑色物件不可能直接(不經過灰色物件) 指向某個白色物件,
- 灰色:表示物件已經被垃圾收集器訪問過,但這個物件上至少存在一個參考還沒有被掃描過,
關于可達性分析的掃描程序,可以發揮一下想象力,把它看作物件圖上一股以灰色為波峰的波紋從黑向白推進的程序,如果用戶執行緒此時是凍結的,只有收集器執行緒在作業,那不會有任何問題,但如果用戶執行緒與收集器是并發作業呢?收集器在物件圖上標記顏色,同時用戶執行緒在修改參考關系(即修改物件圖的結構),這樣可能出現兩種后果,
- 一種是把原本消亡的物件錯誤標記為存活(即原本應該是白色的物件被誤標為黑色),這不是好事,但其實這種情況是可以容忍的,只不過產生了一點逃過本次收集的浮動垃圾而已,下次收集清理掉就好,
- 另一種是把原本存活的物件錯誤標記為已消亡(即原本應該是黑色的物件被誤標為白色),這就是非常致命的后果了,程式肯定會因此發生錯誤,下圖演示了這樣的致命錯誤具體是如何產生的,

Wilson 于 1994 年在理論上證明了,當且僅當以下兩個條件同時滿足時,會產生 “物件消失” 的問題,即原本應該是黑色的物件被誤標為白色:
- 賦值器插入了一潭訓多條從黑色物件到白色物件的新參考;
- 賦值器洗掉了全部從灰色物件到該白色物件的直接或間接參考,
因此,我們要解決并發掃描時的物件消失問題,只需破壞這兩個條件的任意一個即可,由此分別產生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB),
- 增量更新要破壞的是第一個條件,當要插入黑色物件指向白色物件的參考關系時,就將這個新插入的參考記錄下來, 等并發掃描結束之后,再將這些記錄過的參考關系中的黑色物件為根,重新掃描一次,這可以簡化理解為,黑色物件一旦新插入了指向白色物件的參考之后,它就變回灰色物件了,
- 原始快照要破壞的是第二個條件,當要洗掉灰色物件指向白色物件的參考關系時,就將這個要洗掉的參考記錄下來, 等并發掃描結束之后,再將這些記錄過的參考關系中的灰色物件為根,重新掃描一次,這可以簡化理解為,無論參考關系洗掉與否,都會按照剛剛開始掃描那一刻的物件圖快照來進行搜索,
以上無論是對參考關系記錄的插入還是洗掉,虛擬機的記錄操作都是通過寫屏障實作的,
在 HotSpot 虛擬機中,增量更新和原始快照這兩種解決方案都有實際應用,譬如,CMS 是基于增量更新來做并發標記的,G1、Shenandoah 則是用原始快照來實作,
參考資料
《深入理解Java虛擬機》第 3 章:垃圾收集器與記憶體分配策略 3.4.6 并發的可達性分析
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/17305928.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549732.html
標籤:其他
