【Java 8 GC 调优】Garbage-First(G1)

G1 是一个 服务器风格(Server-Style)的垃圾收集器。其适用目标是具有大内存的多处理器计算机。它试图在实现高吞吐量的同时,高概率地满足 GC暂停时间 目标。整个堆操作,如 全局标记,与业务线程同时运行。这可以防止 中断的发生概率 与 堆或存活数据大小 成比例。

G1 通过几种技术实现了 高性能 和 暂停时间 目标。

堆被划分成一些大小相等的区域,每个区域都是一片连续的虚拟内存(virtual memory)空间。G1 会执行一个并发的全局标记阶段,以确定整个堆中对象的存活情况。标记阶段结束后,G1 知道哪些区域大部分是空的(即存活对象较少)。它会首先收集这些区域;这通常会产生大量的可用空间。这也是为什么这种方法会被称为 “G1(Garbage-First)”。顾名思义,G1 将其 收集与压缩 活动集中在充满可回收对象(即垃圾)的区域。G1 使用 “暂停预测模型” 来满足用户定义的暂停时间目标,并基于指定的暂停时间目标选择要收集的区域数量。

G1 将一个或多个区域中的对象复制到单个区域中。在这个过程中会进行压缩并释放内存。此转移操作在多处理器上并行执行,以降低暂停时间,并提高吞吐量。因此,每次GC,G1 都会持续工作,来减少碎片。这超出了前面两种GC的能力。CMS(Concurrent Mark Sweep)不会进行压缩。(并行GC的)并行压缩只会以 整堆 的方式压缩,导致相当长的暂停时间。

需要注意的是,G1 不是 实时收集器。它可以高概率地达到暂停时间目标,但是并不绝对。基于之前收集的数据,G1 会估算出在目标时间内可以收集多少个区域。因此,收集器对于收集区域的成本有一个相当精确的模型,并用它来确定在目标暂停时间内收集哪些区域和区域数量。

G1 的第一重点是为那些需要大容量堆和有限GC延迟的应用程序的运行者提供解决方案。这意味着堆大约有6GB或更大,且稳定可预测的暂停时间低于 0.5 秒。

目前使用 CMS 或 并行GC 的应用程序,如果具有以下特征,那么将从切换到 G1 中收益:

  • Java 堆超过 50% 的空间被存活对象占用
  • 对象的 分配率 或 升迁率 差异显著
  • 应用程序遇到意外的长时间GC暂停或压缩暂停(超过 0.5 到 1 秒)

G1 被计划作为 CMS(Concurrent Mark Sweep)的长期替代品。与 CMS 相比,G1 是一个更好的解决方案。有一个区别是,G1 是一个压缩收集器。此外,G1 提供了比 CMS 更可预测的GC暂停,并允许用户指定所需的暂停时长目标。

与 CMS 一样,G1 是为GC暂停时间要求更短的应用程序设计的。

如下图所示,G1 将堆划分为固定大小的区域(灰色框):

【Java 8 GC 调优】Garbage-First(G1)
G1 划分的堆

从逻辑上讲,G1 是分 “代”的。一组空白区域区域被命名为逻辑上的年轻代。在上图中,年轻代是浅蓝色的。内存分配是在年轻代中完成的,当年轻代满时,这组区域会被回收(一次 Yong GC)。在某些情况下,年轻代区域之外的区域(深蓝色的老年代区域)也会同时被回收。这被称为混合收集。上图中,正在被收集的区域被标记为红色。该图展示了一次混合收集,因为年轻代区域和老年代区域都正在被收集。该GC是一个压缩GC,存活对象被复制到指定的空区域。根据幸存对象的年龄,可将对象复制到 Survivor 区(被标记为 “S”)或一个老年代区(未特别展示)。标有 “H” 的区包含了 “大对象(Humongous Object)”(大于半个区),并被特殊对待。详见“大对象和大分配”。

 

分配(转移)失败

Allocation(Evacuation)Failure

与 CMS 一样,G1 会在业务线程运行期间执行部分收集工作。这存在风险:分配对象的速度比GC恢复可用空间的速度快。可参考《CMS》的 “并发模式失败” 一节中类似的CMS行为。在 G1 中,当 G1 将存活数据从一个区域复制到另一个区域(转移)时,可能会失败(耗尽Java堆)。复制是为了压缩存活数据(内存对齐,不留间隙)。如果在GC的转移过程中无法找到空闲区域,那么会发生 “分配失败”(因为没有空间从被转移的区域分配对象),并会执行一次 STW(stop-the-world)模式的完整GC。

 

浮动垃圾

对象可能在 G1 收集期间死亡(不可达),并不被收集。G1 使用了一种被称为 “起始快照(SATB,snapshot-at-the-beginning)” 的技术,确保可以找到所有存活对象。SATB 将并发标记(对整个堆的标记)开始时的任何活对象都认定为GC的存活对象。SATB 允许浮动垃圾,处理方式类似于 CMS 的增量更新。

 

暂停

G1 会暂停业务线程,以便将存活对象复制到新区域。这些暂停可以是 Yong GC 暂停(只收集年轻代),也可以是 混合GC 暂停(年轻代区域 和 老年代区域 都会被收集)。与 CMS 一样,当业务线程停止后,有一个 最终标记或重标记 暂停来完成标记。但是 CMS 还有一个初始标记暂停,而 G1 将初始标记的工作作为 转移暂停 的一部分。G1 在收集的结尾处有一个 清理阶段,部分是 STW 的,部分是并发的。清理阶段的 STW 部分会标识出空区域,并确定下一GC的候选旧区域。

 

卡表和并发阶段

Card Table and Concurrent Phases

如果GC不是收集整个堆,如 增量式GC,那么GC需要知道 不收集部分的哪些地方 有 指向正要收集的部分。这通常是分“代”GC的特征,其中不收集的部分是老年代,被收集的部分是年轻代。用于保存这些信息的数据结构(老年代中指向年轻代对象的指针)是一个 Remembered Set。卡表(Card Table)是一种特殊类型的 remembered set。Java Hotspot VM 用一个字节数组来作为卡表。每个字节称为一张卡。一张卡对应堆中一个范围的地址。把一张卡“弄脏”表示将其字节的值改为一个“脏值”。“脏值”可能包含了 此卡所覆盖的地址范围内 从老年代指向年轻代 的一个新指针。

处理一张卡表示 查看该卡上是否有从老年代指向年轻代的指针,并可能对此信息做一些操作,例如将其传输到另一种数据结构。

G1 有一个 并发标记 阶段,标记应用程序中找到的存活对象。该并发标记操作会从“转移暂停”的结尾(初始标记工作已完成)延续到重标记。并发清理 阶段会将GC清空的区域 添加到空闲区列表中,并清理这些区域的 remember set。此外,还将根据需要运行一个并发优化线程,以处理被应用程序写操作“弄脏” 且 可能存在跨区引用 的卡表条目。

 

启动一次并发GC周期

如前所述,在一次混合GC中年轻代和老年代区域都会被收集。为了收集老年代,G1 会对堆中的存活对象做一次完整的标记。这种标记是通过一个 并发标记 阶段来完成的。当整个Java堆的占用率达到参数 InitiatingHeapOccupancyPercent 的值时,会启动一次并发标记阶段。可通过命令行选项 -XX:InitiatingHeapOccupancyPercent=<NN> 来设置此参数值。其默认值为 45。

 

暂停时长目标

可通过标记 MaxGCPauseMillis 来设置 G1 的暂停时长目标。G1 使用一个预测模型 来决定 目标暂停时长内可以完成多少GC工作。在GC结尾处,G1 会选择下一次GC要收集的区域。此区域集合包含年轻代区域(其大小之和决定了逻辑年轻代的大小)。年轻代区域数量的选择是 G1 控制GC暂停时长 的手段之一。同其它GC一样,你可以通过命令行指定年轻代的大小。但这么做可能会妨碍 G1 达成暂停时长目标的能力。除暂停时长目标外,你还可以指定(可能)发生暂停的时间跨度。你可以和暂停时长目标一起,指定此时间段所使用的最小变异因子(GCPauseIntervalMillis)。MaxGCPauseMillis 的默认值是200毫秒。GCPauseIntervalMillis的默认值(0)相当于对时间跨度没有要求。