G1的RSet解讀

RSet介紹

在G1中,引入了RSet(Remember Set,記憶集)的概念,用來記錄不同代際之間的引用關係,目的是爲了加快垃圾回收的速度。

通常有兩種方法記錄引用關係,分別爲point out和point in。比如a=b(a引用b),若採用point out結構,則在a的RSet中記錄b的地址;若採用point in結構,則在b的RSet中記錄a的地址。

G1的RSet採用的是point in結構,即誰引用了我。(Card Table採用的是point out結構)

爲什麼需要記錄跨代的引用

JVM一般都會對內存進行分代處理,以提高內存分配和垃圾回收的效率。Minor GC只會回收年輕代,Major GC只會老年代,無論是哪一種GC都會面臨跨代引用的情況,比如老年代對象引用新生代或者新生代對象引用老年代。

Minor GC在回收年輕代時,需要判斷年輕代的對象是否存活,而年輕代的部分對象可能被老年代的對象引用,因此必須掃描老年代纔不會發生誤判年輕代的對象爲垃圾;同理,在回收老年代時,也需要掃描年輕代。

那麼無論是隻回收新生代還是老年代,都需要掃描其他代的對象,相當於進行全堆掃描,效率很低。那麼將代際之間的引用關係記錄在一個單獨的地方,只需要掃描這個地方即可,避免全堆掃描。

RSet帶來的問題

  1. RSet需要額外的內存空間來存儲這些引用關係,一般是JVM最大的額外開銷的1%-20%之間;
  2. RSet中的對象可能已經死亡,那麼這個時候引用的對象會被認爲活躍對象,實際上它是浮動垃圾;
  3. RSet是通過寫屏障來完成的,即在內存分配的地方,插入一段代碼來執行RSet的更新,如果對象的創建/修改/回收比較頻繁,那麼寫RSet的性能開銷還是比較大的。因此一般不會記錄年輕代到老年代的引用。

G1的RSet設計

主要分析哪些引用的關係需要記錄在RSet中;

  • 分區內部的引用

    無論是新生代還是老年代的分區內部的引用,都不需要記錄引用關係。因爲是針對一個分區進行的垃圾回收,要麼這個分區被回收,要麼不被回收。

  • 新生代引用新生代

    G1的三種回收算法(YGC/MIXED GC/FULL GC)都會全量處理新生代分區,所以新生代都會被遍歷到。因此無需記錄這種引用關係。

  • 新生代引用老年代

    無需記錄。G1的YGC回收新生代,無需這個引用關係。混合GC時,G1會採用新生代分區作爲根,那麼在遍歷新生代分區時就能找到老年代分區了,無需這個引用關係。對於FGC來說,所有分區都會被處理,也無需這個引用關係。

  • 老年代引用新生代

    需要記錄。YGC在回收新生代時,如果新生代的對象被老年代引用,那麼需要標記爲存活對象。即此時的根對象有兩種,一個是棧空間/全局變量的引用,一個是老年代到新生代的引用。

  • 老年代引用老年代

    需要記錄。混合GC時,只會回收部分老年代,被回收的老年代需要正確的標記哪些對象存活。

RSet的更新

寫屏障即在改變特定內存的值時,執行一些額外的動作。

G1的RSet的更新是通過寫屏障完成的,在寫變更時,通過插入一條額外的代碼把引用關係放入到DCQ隊列中,隨後refine線程取出DCQ隊列的引用關係,更新RSet。比如,每一次將一個老年代對象的引用修改爲指向新生代對象時,都會被寫屏障捕獲,並且記錄下來。

對於一個寫屏障來時,過濾掉不必要的寫操作是十分必要的,G1進行以下過濾:

  1. 不記錄新生代到新生代的引用 或者 新生代到老年代的引用
  2. 過濾一個分區內部的引用
  3. 過濾空引用

參考

Major GC要不要掃描年輕代對象?

老大難的GC原理及調優,這下全說清楚了

Java性能 – GC

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章