Featured image of post G1 垃圾收集器基本介绍

G1 垃圾收集器基本介绍

简单介绍 G1 垃圾收集器,包括几个重要概念以及 GC 的两种 GC 方式

G1(Garbage-First)JDK7-u4推出商用的垃圾收集器,早在JDK 6u14就有Early Access版本的G1收集器供开发人员实验、试用,而在最近的JDK9中已经把G1设置为默认的垃圾收集器,G1垃圾收集器从其内存结构上就逐渐和传统的垃圾收集器有着一些区别,所以本文想着针对G1垃圾收集器做一些简单原理上的介绍。

内存结构

首先,在内存结构上,传统垃圾收集器一般都是以下面左边图片那样的结构,将整个堆空间分成新生代、老年代以及永久代(java8之后改成元空间)构成,这些区域在物理上属于连续的空间,且大小比例一般情况下是固定的,这样带来的弊端随着内存的增大就越来越显现出来:它的一次GC的时间将会越来越长。

G1则将整个堆空间划分为一个个大小相等的小块(每一小块称为一个RegionRegion的大小范围为1M~32M,大概有2000个左右,且每个区域大小都是2的整数次幂,可以通过-XX:G1HeapRegionSize设置大小),每一块区域的内存是连续的。和传统垃圾收集器一样,G1中也存在分代收集的概念,每一个Region中所充当的角色可以是EdenSurvivorOld三种中的一种,角色不固定,这样使得整个内存空间的使用更加灵活。

另外,在G1的堆空间还存在这一种类型为HumongousRegion区域,该区域用来存储的是大型对象,即大小大于等于普通Region一半的对象,同时如果该对象大小超过一个Region,那么就会由多个Humongous拼接存放。该对象属于老年代,垃圾收集算法中,大对象直接进入老年代就是指的进入G1的该区域。

基本概念

Card Table

我们知道在执行YGC的时候,其目的是回收掉年轻代中所有的不再存活的对象,而如何判断该对象不再存活,就是通过GC Roots,那么在执行YGC的时候,我们就可以认为老年代中的对象是GC Roots,所以如果存在老年代引用年轻代的情况,我们就不能对该对象进行回收。所以这里如果是一般做法,可能就是在执行YGC的阶段,扫面整个老年代,查看老年代引用了哪些年轻代,这样的操作在老年代空间较小的时候,姑且还能够进行,但是如果老年代很大,有好几个G,那么显然是不可能这样操作的。

JVM 采用了一种叫 CardTable(卡表)的数据结构来解决这个问题。

图片来源 春风得意马蹄疾,一文看尽JVM

卡表就是一个 bit 数组,元素默认值为0。从上图可以看出,Old区被等分成了多个区域,每个区域对应卡表上的一个位置,如果某个区域中有对象引用了 Young 区的对象,则这个区域在卡表中对应的位置的值被设为 1。这样JVM在进行垃圾回收的时候,就可以依据cardtable中值是否为1来进行专门的扫描任务,减少耗时。

Remember Set(Rset)

RSet是在card table的基础上设计出来的新的数据结构,所以其作用主要也是为了提升GC性能,相比于card table,它是一个HashTable的结构,其记录的是哪些对象引用了当前对象,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,即Card对应的Index,映射到对象的Card地址。

当每一个Region进行初始化的时候,都会新建一个RSet,而当一个Region内的对象,被其他Region中引用的时候,就会遇到一个问题,如何判断当前对象被哪些Region内的对象引用了?

这时,依靠在每次引用类型对象尽心写操作的时候,产生一个Write Barrier(写屏障,详细信息可以阅读垃圾回收(3)G1的结构和概念中的RSet实现部分有介绍)的暂停中断,然后检查将要写入的引用指向的对象是否和该引用类型数据在同一个Region中(其他收集器:检查老年代对象是否引用了新生代对象),如果不同,通过card table将相关引用信息记录到引用指向对象的所在Region对应的RSet中,避免针对整个堆中的所有Region进行遍历。

Collection Set(CSet)

CSet主要是记录需要回收的Region集合,在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。CSet针对年轻收集和混合收集,其工作机制都是一致的,年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。

SATB

STAB全称是Snapshot-At-The-Begining,通过字面理解,它是在GC开始的时候,通过Root Tracing得到一个快照,该作用是为了维持并发GC的正确性。

那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记了,但是它的field还没有被标记或标记完。
  • 黑:对象被标记了,且它的所有field也被标记完了。

那么在回收线程和程序执行线程同时运行的并发阶段,就会存在之前已经被标记过的对象状态发生变化,导致引用关系发生变化,比如一个被白色标记的对象,从被灰色标记的对象引用变成了被黑色标记的对象进行引用。如果不进行处理,那么这个白色对象就会因为漏掉而被错误回收,从而导致程序错误。而SATB算法就可以解决这样的问题,其详细实现思路不在本篇文章的介绍之内,可以参考G1 SATB和Incremental Update算法的理解理解。

工作流程

G1 的垃圾收集分为两种:Young GC Mixed GC

Young GC

Young GC 只会涉及到 Young Regions,它将 Eden Region 中存活的对象移动到一个或多个新分配的 Survivor Region,之前的 Eden Region 就被归还到 Free list,供以后的新对象分配使用。

当区域中对象的 Survive 次数超过阈值(TenuringThreshold)时,Survivor Regions 的对象被移动到 Old Regions;否则和 Eden 的对象一样,继续留在 Survivor Regions 里。

Mixed GC

多次 Young GC 之后,Old Regions 慢慢累积,直到到达阈值(InitiatingHeapOccupancyPercent,简称 IHOP),虚拟机不得不对 Old Regions 做收集。这个阈值在 G1 中是根据用户设定的 GC 停顿时间动态调整的,也可以人为干预。

Old Regions 的收集会同时涉及若干个 YoungOld Regions,因此被称为 Mixed GCMixed GC 很多地方都和 Young GC 类似,不同之处是:它还会选择若干最有潜力的 Old Regions(收集垃圾的效率最高的 Regions),这些选出来要被 Evacuate(将一个活的对象从一个区域拷贝到另外一个区域) 的 Region 称为本次的 Collection Set (CSet)

Mixed GC 的重要性不言而喻:Old Regions 的垃圾就是在这个阶段被收集掉的,也正是因为这样,Mixed GC 是工作量最为繁重的一个环节,如果不加以控制,就会像 CMS 一样发生长时间的 Full GC 停顿。这时候 Region 的设计就发挥出优越性了:只要把每次的 Collection Set 规模控制在一定范围,就能把每次收集的停顿时间软性地控制在 MaxGCPauseMillis 以内。起初这个控制可能不太精准,随着 JVM 的运行估算会越来越准确。

那来不及收集的那些Region呢?多来几次就可以了。所以你在 GC 日志中会看到 continue mixed GCs 的字样,代表分批进行的各次收集。这个过程会多次重复,直到垃圾的百分比降到 G1HeapWastePercent 以内,或者到达 G1MixedGCCountTarget 上限。

对于 Young Regions,我们对它有以下特殊优化:

  1. Evacuation 的时候,Young Regions 一定会被放到待收集的 Regions 集合(Collection Set)中,原因很简单,绝大多数对象寿命都很短,在 Young Regions 做收集往往绝大部分都是垃圾。
  2. 由于 Young Regions 一定会被收集,我们获得了一个可观的收益:Remember Set 的维护工作不需要考虑 Young 内的引用修改(换句话说RSet只关心 old-to-youngold-to-old的引用),当 Young Region 上发生 Evacuation 时我们再去扫描并构建出它的 RSet 即可。

总结

本篇文章简单介绍了一下G1垃圾收集器的基本概念、关键概念以及垃圾收集的基本流程,并没有作深入的介绍具体的实现细节,后续考虑通过多篇文章介绍局部实现细节。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus