序
G1(Garbage-First)
是JDK7-u4
推出商用的垃圾收集器,早在JDK 6u14
就有Early Access
版本的G1
收集器供开发人员实验、试用,而在最近的JDK9
中已经把G1
设置为默认的垃圾收集器,G1
垃圾收集器从其内存结构上就逐渐和传统的垃圾收集器有着一些区别,所以本文想着针对G1
垃圾收集器做一些简单原理上的介绍。
内存结构
首先,在内存结构上,传统垃圾收集器一般都是以下面左边图片那样的结构,将整个堆空间分成新生代、老年代以及永久代(java8
之后改成元空间)构成,这些区域在物理上属于连续的空间,且大小比例一般情况下是固定的,这样带来的弊端随着内存的增大就越来越显现出来:它的一次GC
的时间将会越来越长。
而G1
则将整个堆空间划分为一个个大小相等的小块(每一小块称为一个Region
,Region
的大小范围为1M~32M
,大概有2000
个左右,且每个区域大小都是2
的整数次幂,可以通过-XX:G1HeapRegionSize
设置大小),每一块区域的内存是连续的。和传统垃圾收集器一样,G1
中也存在分代收集的概念,每一个Region
中所充当的角色可以是Eden
、Survivor
、Old
三种中的一种,角色不固定,这样使得整个内存空间的使用更加灵活。
另外,在G1
的堆空间还存在这一种类型为Humongous
的Region
区域,该区域用来存储的是大型对象,即大小大于等于普通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
的收集会同时涉及若干个 Young
和 Old Regions
,因此被称为 Mixed GC。Mixed 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
,我们对它有以下特殊优化:
Evacuation
的时候,Young Regions
一定会被放到待收集的Regions
集合(Collection Set
)中,原因很简单,绝大多数对象寿命都很短,在Young Regions
做收集往往绝大部分都是垃圾。- 由于
Young Regions
一定会被收集,我们获得了一个可观的收益:Remember Set
的维护工作不需要考虑Young
内的引用修改(换句话说RSet
只关心old-to-young
和old-to-old
的引用),当Young Region
上发生Evacuation
时我们再去扫描并构建出它的RSet
即可。
总结
本篇文章简单介绍了一下G1
垃圾收集器的基本概念、关键概念以及垃圾收集的基本流程,并没有作深入的介绍具体的实现细节,后续考虑通过多篇文章介绍局部实现细节。