上一章記錄了幾種常見的垃圾收集器,見《第五章 JVM垃圾收集器(1)》 1、G1 說明: 從上圖來看,G1與CMS相比,僅在最後的"篩選回收"部分不同(CMS是併發清除),實際上G1回收器的整個堆記憶體的劃分都與其他收集器不同。 CMS需要配合ParNew,G1可單獨回收整個空間 原理: G1收集器將
上一章記錄了幾種常見的垃圾收集器,見《第五章 JVM垃圾收集器(1)》
1、G1
說明:
- 從上圖來看,G1與CMS相比,僅在最後的"篩選回收"部分不同(CMS是併發清除),實際上G1回收器的整個堆記憶體的劃分都與其他收集器不同。
- CMS需要配合ParNew,G1可單獨回收整個空間
原理:
- G1收集器將整個堆劃分為多個大小相等的Region
- G1跟蹤各個region裡面的垃圾堆積的價值(回收後所獲得的空間大小以及回收所需時間長短的經驗值),在後臺維護一張優先列表,每次根據允許的收集時間,優先回收價值最大的region,這種思路:在指定的時間內,掃描部分最有價值的region(而不是掃描整個堆記憶體),並回收,做到儘可能的在有限的時間內獲取儘可能高的收集效率。
運作流程:
- 初始標記:標記出所有與根節點直接關聯引用對象。需要STW
- 併發標記:遍歷之前標記到的關聯節點,繼續向下標記所有存活節點。
- 在此期間所有變化引用關係的對象,都會被記錄在Remember Set Logs中
- 最終標記:標記在併發標記期間,新產生的垃圾。需要STW
- 篩選回收:根據用戶指定的期望回收時間回收價值較大的對象(看"原理"第二條)。需要STW
優點:
- 停頓時間可以預測:我們指定時間,在指定時間內只回收部分價值最大的空間,而CMS需要掃描整個年老代,無法預測停頓時間
- 無記憶體碎片:垃圾回收後會整合空間,CMS採用"標記-清理"演算法,存在記憶體碎片
- 篩選回收階段:
- 由於只回收部分region,所以STW時間我們可控,所以不需要與用戶線程併發爭搶CPU資源,而CMS併發清理需要占據一部分的CPU,會降低吞吐量。
- 由於STW,所以不會產生"浮動垃圾"(即CMS在併發清理階段產生的無法回收的垃圾)
適用範圍:
- 追求STW短:若ParNew/CMS用的挺好,就用這個;若不符合,用G1
- 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面沒有優勢
2、幾點註意
問題1、G1以外的其他收集器在回收垃圾的時候:要不只是掃描年輕代,要不只是掃描年老代。在年輕代的回收過程中,如果舊生代中的對象引用了年輕代的對象,那麼我們只掃描年輕代就不行了,但是由於年老代一般而言是年輕代的3倍大小,如果年輕代、年老代一起去掃描的話,效率會急劇下降,這個問題怎麼處理?
答:JVM採用remember set來做的這個事兒,當發現一個引用對象被賦值時,JVM發出一個write barrier指令來暫時中斷寫操作,檢查被賦值的引用對象是不是處於年老代,且其引用的對象是不是處於新生代(即是不是年老代對象引用了年輕代對象),如果是,將相關引用信息記錄到remember set。之後的掃描,我們會從根集合+remember set向下進行掃描。(也就是說真正的根集合,是JVM定義的根集合+remember set)
問題2、G1收集器為了做到GC時間可預測,採用掃描部分價值最大的region來實現,那麼如果這部分region中的對象被其他region中的對象所引用,那麼僅掃描前者可能就不行了,但是如果掃描全部region的話,又無法做到GC時間可預測,效率會大大下降,怎麼辦?
答:G1同理,為每一個region分配一個remember set,當發現一個引用對象被賦值時,JVM發出一個write barrier指令來暫時中斷寫操作,檢查被賦值的引用對象與其引用的對象是不是處於不同的region(eg.a=b;檢查a與b是不是在不同的region),如果是,將相關引用信息記錄到當前region的remember set。之後的掃描,我們會從根集合+remember set向下進行掃描。
問題3、CMS與G1在併發標記的時候若發部分引用對象的引用關係發生了變化,怎麼處理才能讓重新標記的時候僅僅掃描出這些變化?
答:在併發標記期間,對象的引用關係若發生了變化,這些相關的記錄就會記錄到remember set logs;在重新標記階段,將該logs的信息加入到remember set中去,然後再從remember set去向下trace節點。