垃圾回收器的两个指标

响应速度(Responsiveness)

系统对于输入数据的响应快慢,例如:

  • web服务器返回请求所消耗的时间
  • 一个数据库查询请求所消耗的时间

对于关注响应速度的系统而言,长时间的pause是不可接受的。

吞吐量(Throughput)

系统在指定的时间段内所能处理最大数量的任务。例如:

  • 在一个指定的时间段内系统所能完成的事务数量
  • 批处理系统中一个小时内完成的任务数量
  • 一个小时内能够完成的数据库查询总数

吞吐量指标通常关心的是一段较长时间范围内JVM的整体表现,因此单次Pause时间较长是可以被接受的。

G1 垃圾回收器

设计目标:运行于多处理器、大内存计算机上的垃圾回收器,兼顾了停顿时间(响应速度)与吞吐量。

我们可以通过参数-XX:MaxGCPauseMillis 设置JVM在GC时最大的停顿时间,G1垃圾收集器将根据自身预测模型”尽力”满足这一条件(G1垃圾回收器并不保证一定能将GC停顿时间控制在这一范围内)

设计特点:

  • 像CMS一样可以并发执行垃圾回收操作
  • 不会消耗冗长的时间用于压缩空闲空间
  • 可更精确地预测GC停顿耗时
  • 不会消耗太多的吞吐量表现(也就是说G1仍然是以响应速度为基准的,吞吐量被放在第二位)
  • 不会消耗过于夸张的JVM堆空间

官方建议遇到以下情况的CMS或者ParallelOldGC切换为G1:

  1. Full GC耗时非常长或者非常频繁
  2. 对象分配率或者提升量非常显著
  3. 垃圾收集或压缩的耗时特别长

G1工作原理

内存空间分配

与一般的垃圾回收器不同,G1的堆内存空间分配不在是连续的Eden、Survivor、Tenured(Old),同时,Eden、Survivor与Tenured之间的默认比例关系也不再一样。

old memory

G1垃圾回收器将内存划分为许多个连续的小空间(Region),这些独立的小空间根据用途的不同,分别被打上Eden、Survivor、Old以及Humongous的标签。如下图所示

region memory

通常情况下G1将把堆空间分为2048个不同的region, 每个region的大小在1MB- 32MB之间,region大小可通过参数-XX:G1HeapRegionSize=n进行配置。所以G1可支持的堆空间大小最大为2000 * 32MB = 64 GB;最小可少于2GB,此时堆空间将少于2048个region。

G1垃圾回收算法中新生代(Eden+Survivor)与老年代(Tenured)空间的划分通过参数-XX:G1NewSizePercent,-XX:G1MaxNewSizePercent 进行配置。JVM初始化完成时,新生代空间占全部堆空间的比例由参数-XX:G1NewSizePercent决定(默认5%), 经过不断的扩张与垃圾收集后,新生代最多可占全部堆空间的-XX:G1MaxNewSizePercent(默认60%)。

Humongous是一种比较特殊的老年代Region,用于存储超大对象(单个对象超过region大小的50%及以上),由于处理逻辑与老年代差不多,这类region的存活时间通常较长,回收的成本较高。实际开发过程中我们应尽量避免使用到这类空间。实在避免不了的情况下,可通过设置更大的region空间大小予以规避。

GC类型分类

当使用G1垃圾回收器时,JVM可能存在3种GC模式:

  • YoungGC:JVM仅回收新生代中的内存空间
  • MixGC:JVM选择老年代中的部分region,以及新生代的全部region进行回收
  • FullGC:当JVM中垃圾回收的速度跟不上对象分配的速度时,JVM将Stop The World,采用最原始的SerialGC对整个堆进行垃圾回收

YoungGC(Stop The World)

所有的垃圾回收都是从YoungGC开始的,MixGC与FullGC都只会发生在YoungGC(Minor GC)完成后。YoungGC的触发条件有且仅有1个:JVM分配空间时发现Eden区域剩余空间无法满足此次对象分配。

G1中的YoungGC整个过程都是Stop The World的,使用标准的可达性算法进行垃圾回收:JVM将对所有Eden区域中的对象进行一次扫描,确认该对象是否”Root可达”,然后将仍然”存活”(即可达)的对象拷贝至Survivor Region中,达到生命周期阈值的对象将从Survivor Region提升至Old Region。

这里的核心问题是:要得到完整的对象引用关系图,就必须对JVM堆中的对象及对象关系进行一次遍历。

card-table

如上图所示,当我们需要对新生代中的对象进行垃圾回收时,应当保留从root直接可达的Y-Object1以及间接可达的Y-Object2,回收掉无法到达的Y-Object3。Root->Y-Object1的可达性很好确认,但是间接可达的Y-Object2涉及到老年代中的对象O-Object1,在仅扫描新生代空间的情况下无法得到这样的引用信息。因此,G1垃圾回收器引入了空间换时间的RememberSet,简称RSet。(所有分代垃圾收集器都有类似的空间换时间机制,一般是通过CardTable实现,G1中同样包含CardTable)

remember-set

如上图所示,G1 的Eden Region与Old Region都会配备一块额外的空间Remember Set,用于保存其他Old Region中的对象引用当前Region中对象的信息。这样,只需要将Region的Remember Set中所指向的对象也当做Root根节点处理,即可避免扫描老年代空间,节省大量的时间开销。

Tips: 分代垃圾回收与CardTable实现请参考分代垃圾回收与CardTable

全局并发标记

当YoungGC完成时发现老年代中的使用空间比例大于-XX:InitiatingHeapOccupancyPercent所设置的阈值(默认45%)时,G1就会启动全局并发标记过程,并根据实际情况准备进行MixGC。

此阶段将递归地标记老年代中所有存活对象,并计算出每个Region中存活对象占总内存空间的大小。

全局并发标记主要由初始标记、并发标记、最终标记、清理四个阶段组成。

Tips:垃圾回收标记通常采用三色标记法

初始标记 initial marking

Stop The World 初始标记阶段往往发生在Young GC之后,与此次YoungGC共享Stop The World时间。在这个阶段中,G1 会扫描根集合直接可达的对象,以这些对象作为并发标记阶段的起点。除了JVM中普通的root直接可达对象外,此阶段还会标记那些被Survivor区域直接可达的对象,由于刚进行了一次YoungGC,这些对象获取起来简单,速度很快,不需要使用RSet进行记录。

并发标记 concurrent marking

多个线程并发、递归地扫描整个堆的对象引用图,标记所有可达对象。该阶段不会暂停JVM,GC线程将与工作线程并发执行。

G1核心思路:SATB(snapshot-at-the-beginning):一次GC开始的时候可达的对象将会被认为是活的,此时的对象图形成一个逻辑“快照”(snapshot);在GC过程中新分配的对象都当作是活的。(虽然都叫并发标记,G1的实现与CMS略有不同)。

简单来说有两点:1.在并发标记阶段新创建的对象都是”存活的”;2.所有被修改了引用关系的对象都将被认为是存活的。

Tips:关于SATB的细节实现请参考Incremental update Vs. SATB

最终标记 final marking / remarking

Stop The World 从指定的log数据结构中找到还未标记完成的对象(对象引用关系修改),继续完成剩余标记工作。

清理 cleanup

Stop The World 对所有Region进行统计和整理(不涉及对象拷贝)并重置并发标记阶段中生成的各种中间状态和中间值,为下一次标记做准备。对于无存活对象的Region,G1会直接将其标记为可分配状态(free);对于仍有存活对象的Region,G1将统计其存活对象占总空间的大小,为Evacuation阶段Region的选择提供参考。

Evacuation

Evacuation阶段将采用复制-清理的方式对Region空间进行清理,将多个Region中的存活对象拷贝至其他空的Region中,并把这些Region的空间释放出来,如下图所示:

Evacuation

在MixGC模式下,G1首先会根据之前已经停顿的时间以及用户所设置的期望停顿时间得到本次Evacuation大约能够持续的时间T;再结合停顿预测模型所得到的平均每个Region清理耗时t,得到本次GC清理的Region数量:T / t。

在选择Region时,G1总是倾向于选择死亡对象空间/Region总空间比例较大的Region,因为死亡对象所占的空间越大,意味着清理该Region所释放的空间也就越大,清理的收益也就越大。

Tips:G1停顿预测模型具体实现:G1停顿预测模型

总结

以CMS替代者的角度而言,G1达到了其设计之初的目标:参数较少,调试简单,减少内存碎片产生、预防退化为Serial Old整堆扫描等。CMS垃圾回收器基本完成其历史使命。后续无论是Oracle还是OpenJDK都将CMS标记为Deprecated,不再对其进行支持和改进。

作为“驾驭一切”的垃圾收集器,G1恐怕还称之有愧。虽然JDK1.9之后默认的收集器便是G1,但其吞吐量还是逊色于Parallel Scavenge(原理决定,个人觉得恐怕没有垃圾收集器的吞吐率能够超过Parallel Scavenge)。随着时间的推移,硬件的发展也越来越快,G1最大支持64GB内存这一点正在渐渐成为其掣肘。G1的继任者ZGC的测试版本已经面世,垃圾收集器的世代更替仍会继续。

References

[1]深入理解 Java G1 垃圾收集器
[2]Java Hotspot G1 GC的一些关键技术
[3][HotSpot VM] 请教G1算法的原理
[4][HotSpot VM] 关于incremental update与SATB的一点理解