JVM垃圾回收器学习2:G1

jvm,gc,垃圾回收器

Posted by Chris on November 24, 2019

上一篇博客概括过G1之前的其他垃圾回收器的特点,这里开始介绍G1垃圾回收器。

1 G1出现的背景

之前介绍过的新生代和老年代的垃圾收集器,在表现上存在如下问题:

  • 老年代一般比较大,Serial Old和Parallel Old都需要Stop The World,因此,停顿时间可能会比较长。而CMS则会有内存碎片的问题。
  • 需要有新生代和老年代的垃圾收集器一起搭配使用

G1的出现,则可以解决这些问题。G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,在jdk 7u4版本发行时被正式推出。

2 G1的特点和做的事情

G1的定位是在多核、大内存的机器上使用,一般在服务端应用里使用,首要目的是降低用户线程的停顿时间,其次才是追求高吞吐量。

特点如下:

  • 在垃圾收集的过程中,会存在GC线程和用户线程同时工作的场景
  • 更容易控制GC暂停时间,同时保持较高的吞吐量
  • 分代收集。以不同的方式来处理新创建的对象和老对象

3 G1的实现细节

下面先讲述G1用到的一些算法,然后介绍垃圾回收的过程。

Region

G1将整个堆分成大小相等的一个个区域(Region),用来存放对象,如下图所示。有的区域用来存放Eden对象,有的用来存放Survivor对象,有的用来存放Old对象。当遇到大对象时,则可能需要两个Region合并后,才可以放得下该大对象,如图中标记为H(humongous object)的Region。

Remembered Set

每个Region都有对应的Rememered Set,用来存储指向该Region的引用信息,如下图所示。当虚拟机发现应用程序写入Reference类型的数据时,就会产生一个Write Barrier来暂时中断写操作,然后检查该Reference引用的对象是否在同一个Region中。如果不是,则将该引用信息添加到被引用对象的Remembered Set中。例如,Region6中的对象引用了Region1中的对象,那么,Region6就会被记录到Region1对应的Remembered Set 1中。
使用Remembered Set的好处是,在内存回收时,在GC根节点的枚举范围中加入Remembered Set,就可以快速知道Region的引用情况,从而避免对于全堆的扫描。例如要回收某个年轻代的Region,通过Remembered Set就可以快速知道是否有Old Region来引用它,避免了GC root链表的缓慢的扫描。这是一种空间换时间的思想。

SATB(Snapshot-At-The-Beginning)

并发标记之后,如果用户线程更改了对应的引用关系,就会导致并发标记产生漏标和错标的问题。怎么解决这个问题呢?可以用到SATB算法。

SATB全称snapshot-at-the-beginning,由Taiichi Yuasa为增量式标记清除垃圾收集器开发的一个算法,主要应用于垃圾收集的并发标记阶段,解决了CMS垃圾收集器重新标记阶段长时间STW的潜在风险。

可以认为SATB的基础是三色标记算法,三色标记算法用于并发标记的过程。三色标记算法将对象分为三种状态:

  • 白:对象没有被标记到。标记阶段结束之后,会被当做垃圾回收掉。
  • 灰:对象已经被标记了,但是它的field还没有被标记到,或者还没有被标记完。
  • 黑:对象以及field都已经被标记了。

在三色标记的并发标记阶段过程中,由于用户线程和GC线程同时运行,用户线程可能会更改对象的引用关系,因此导致并发标记的结果不准确,产生错标。下面的情况会导致错标的问题:

  • 用户线程让一个黑对象引用一个新的(白)对象。这样会导致这个新的对象漏标,仍然为白色,可能会被回收。
  • 用户线程删除了指向某白对象的所有灰对象。这样会导致GC开始时还活着的对象,到了并发标记时,就变成了死对象。相当于一种漏标的情况。

为了解决错标的问题,SATB算法使用了写屏障来解决这两个问题。每当存在引用更新的情况,G1会将该修改记录下来,然后在最终标记(final marking phase)阶段进行扫描,修正SATB的误差。
可以看出,并发标记采用的SATB算法,只能识别出并发标记开始时那一瞬间的垃圾,而无法识别并发标记过程中产生的垃圾,这也是snapshot at the beginning的含义,即开始并发标记的那一刻的关于垃圾的快照。并发标记阶段只识别“快照”中的垃圾,可以保证垃圾能够被快速的标记,提高标记的效率。

G1回收垃圾的过程

上面说完了G1所用到的垃圾回收的一些算法和内存结构。下面开始讲垃圾回收的阶段。如下图所示。

  • 初始标记(initial marking):标记了从gc root开始的直接关联可达的对象。需要STW(stop the world)
  • 并发标记(concurrent marking):并发标记的过程,会标记整个堆的存活的对象,使用的方法是之前讲过的SATB算法
  • 最终标记(final marking):该阶段标记SATB算法记录的引用变更的对象,修正并发标记的不准确性。
  • 筛选回收(live data counting and evacuation):该阶段计算每个region里面存活的对象。若垃圾太多触发了回收操作(Mixed GC),则动作如下
    • 将完全没有存活对象的Region放入空闲列表
    • 为Eden Region的存活对象创建新的Region(Survivor Region),并移动过去,回收旧的Eden Region
    • Survivor Region的存活对象若经历了15代(默认为15),则移到Old Region。
    • 为Old Region里面的存活对象创建新的Old Region,回收老的Old Region。

在上述的不同阶段,G1使用不同的回收模式,大致分为两种。

  • Young GC
    在初始标记的过程中发生。Young GC的回收对象是年轻代的Region,即Eden Region和Survivor Region。它会将Eden区的对象移动到Survivor区,将经历过多个回收次数的Survivor区的对象移动到Old区。
    Young GC会通过选定年轻代的Region个数来控制Young GC的时间开销。初始标记是stop the world的,所以这个过程也在stop the world的场景下发生。
  • Mixed GC
    当满足以下3个条件任何一下,就会触发一次并发标记,并发标记会统计各个Region的回收价值。
    • Old Regions中的对象占整个堆大小的比例达到45%(默认为45%),比例可以通过参数IHOP(InitiatingHeapOccupancyPercent)来配置。这个时候,说明老对象很多,不能只是清理新生代了。
    • 为转移Eden对象而预留的Survivor空间不够用了,默认预留空间为10%,通过参数G1ReservePercent来配置。 Survivor空间不够用,此时只能拷贝到Old区。此时会触发并发标记。
    • 大对象的场景下
      大对象的拷贝是很费力的,因此为了减少拷贝的消耗,直接将大对象分配到Old区。此时会触发并发标记。

    当统计得到Old Region区的可回收对象超过了5%时(这里比例由参数G1HeapWastePercent控制,在JDK8u40+中默认是5%),就会触发一次Mixed GC。Mixed GC的动作在筛选回收那里讲过了。 Mixed GC的回收范围是所有的年轻代Region以及并发标记统计到的回收价值高的若干Old Region。选定的Old Region的个数根据配置停顿时间来选择。

4 总结

本文简单的讲述了G1垃圾回收器的背景、特点、内存结构和回收过程。希望对于您理解G1有些帮助。

5 参考

G1 垃圾收集器介绍
Java Hotspot G1 GC的一些关键技术
Garbage-First Garbage Collector Part 1: Introduction to the G1 Garbage Collector
G1垃圾收集器之SATB