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

你可能感兴趣的:(G1的RSet解读)