JVM Options

G1 GC 概览

GC 事件类型和触发器

在正常运行期间,GC 是通过需求/活动【need/activity】触发的,例如应用程序线程需要空间但没有可用空间。同时,外部力量也可以引发 Full GC 事件,通常通过 jcmd 和 jmap 进行诊断。常见的触发器有:

  • Eden full
  • 可用空间【Free space 】无法容纳巨型对象
  • 巨型对象已成功分配内存且满足 GC 事件条件【GC event conditions】
  • 外部引发(jcmd, jmap, Runtime.gc())

这里的理想路径是,只有当 Eden 已满或巨型对象已成功分配且满足某些条件时才会触发 GC 事件。此理想路径仅涉及 Minor 事件和 Mixed 事件。Full GC 事件可能会超过最大期望 STW,需要避免。巨型对象通常是一种负担,因为大量的巨型对象将增加耗尽空闲空间的可能性,从而触发 Full GC。

GC 事件的类型如下:

  • Minor: Eden + Survivor From -> Survivor To
  • Mixed: Minor + (# reclaimable Tenured regions / -XX:G1MixedGCCountTarget) regions of Tenured
  • Full GC: All regions evacuated
  • Minor/Mixed + To-space exhaustion: Minor/Mixed + rollback + Full GC

在平稳运行的应用程序中,Minor 事件批次与 Mixed 事件批次交替出现。Full GC 事件和 To-space 耗尽是您在运行 G1 GC 时绝对不想看到的事情,需要检测并消除它们。

对于 http 应用程序和许多其他类型的应用程序,Eden 的疏散(Minor 事件)严格取决于并发性和请求数据大小。换句话说,要减少 Minor STW 时间,要么减少加载/处理/发送【loaded/munged/sent】的数据,要么减少并发性。

因此,为了保持较短的 STW 时间,可变因素是 Tenured 空间的回收。 G1 GC 的方法是将 Tenured 分成小块回收,并且有一整套支持杠杆,可以进行细粒度的控制。

高层视角

在每个 Minor 事件结束时,都会进行检查,以确定是否该考虑清理 Tenured。如果检查通过,则启动多阶段并发标记周期 (MPCMC)。顾名思义,MPCMC 将与应用程序线程并发运行,为每个 Tenured 区域生成一组活跃度元数据【liveness metadata】(可回收空间量)。在活跃度元数据完成后的下一个 GC 事件开始时,将对可回收数据量进行不同的检查。如果可回收数据不足,则活跃度元数据将被丢弃,事件为 Minor 事件。如果可回收数据足够,则 GC 事件为 Mixed 事件。后续 GC 事件也会注意到活跃度元数据的存在,并根据剩余的可回收 Tenured 空间量决定是 Mixed 还是 Minor,任何运行 Minor 事件的决定都会清除活跃度元数据。

详细走查

混合 GC 事件仅在存在活跃度元数据【liveness metadata】时才会发生。活跃度元数据由 MPCMC 运行生成。MPCMC 在 Minor 事件结束时启动。

Minor 事件正在进行中,Eden/From 空间已清空并调整大小,应用程序线程仍处于停止状态。此时,G1GC 将进行检查以确定是否应启动 MPCMC:

当前使用的堆总可用堆 > -XX:InitiatingHeapOccupancyPercent

由于检查是在 Eden/From 被清空后进行的,因此检查基本上等同于是在询问 Tenured 的当前大小是否超过总堆的可配置阈值(默认为 45%)。如果此检查通过,则将获取用于跟踪 Eden/Survivor From 中的指针的数据(线程/堆栈/寄存器/等)的快照并将其提供给 MPCMC,它将在 -XX:ConcGCThreads 上运行。然后,应用程序线程将被放行,MPCMC 和应用程序将并行地处理它们的业务。MPCMC 运行时间范围从 50 毫秒到 5 秒或更长。

MPCMC 的工作类似于 Minor 事件期间所做的工作,即跟踪堆中的指针。但是,在这种情况下,指向 Tenured 的指针才是我们感兴趣的。以这种方式找到的每个 Tenured 对象都被标记为活动对象。同样,与 Minor 事件期间对堆中对象的跟踪类似,未通过跟踪找到的 Tenured 中的对象被视为未引用且不活动,可以回收。在 MPCMC 结束时,每个区域都会有一个活跃度值,即该区域中活动数据占总空间的百分比。收集一个活跃度为 18% 的区域,将使 82% 的区域净增为可用空间。

MPCMC 不会被 Minor GC 事件中断,但是 Full GC 将不需要活动度数据【liveness data】并中止 MPCMC。创建活跃度元数据【liveness metadata】,下一个 GC 事件将注意到活跃度元数据的存在,并进行可回收性检查,以确定 GC 事件应该是 Minor 事件还是 Mixed 事件。如果可回收性检查失败,则丢弃元数据。

那么什么是可收回性检查?

为了确定通过收集 Tenured 可以回收的数据量,G1 GC 首先会创建一个可回收的 Tenured 区域列表。该列表仅包含活跃度 ≤ -XX:G1MixedGCLiveThresholdPercent(默认值为 85)的区域,并遵循基于活跃度的自然排序(即活跃度最低的区域优先)。然后统计通过收集此列表中的区域可以回收的总字节数,并将其除以总堆以获得百分比。如果可回收百分比 ≥ -XX:G1HeapWastePercent(默认值为 5),则可回收性检查通过。

值得注意的是,Tenured 中可回收和不可回收的堆浪费之间存在区别。如果 20% 的堆由活跃度为 86% 的 Tenured 区域组成,那么这将是 2 * .14 = .028,或者大约3%的堆被浪费且不可回收。

如果可回收检查通过,GC 事件将为 Mixed,算法接下来将确定除了 Eden/From 之外还要收集多少和哪些 Tenured 区域。选择要收集哪些 Tenured 区域是该算法名称(G1)的由来,即要收集的区域是垃圾最多的区域(最不活跃,可回收列表顶部的区域)。

确定要收集多少个区域有点复杂,在高层次上有三个步骤,涉及 floor 、adder 和 ceiling。

  • Floor -> 从最低开始,平均分配总工作量
  • Adder -> 如果事件允许,增加更多的区域
  • Ceiling -> 用总堆空间的硬限制来限制区域

区域的起始数量为:

length(Reclaimable Region List) -XX:G1MixedGCCountTarget

如果可以在目标暂停时间 -XX:MaxGCPauseMillis 内完成,则将收集其他 Tenured 区域。最后,需要收集的 Tenured 区域的数量将限制在总堆的 -XX:G1OldCSetRegionThresholdPercent(默认值为 10%)百分比。

多个 Mixed 事件

确定要收集的 Tenured 区域数量后,将从排序好的可回收区域列表的前面弹出该数量的区域,并与 Eden/From 一起收集。由于活跃度元数据【liveness metadata】已经存在,因此在完成 GC 事件之前不会检查是否应启动 MPCMC。GC 事件结束,应用程序线程从中断的地方继续。

在下一个 GC 事件中,同样是在事件开始时,注意到活跃度元数据【liveness metadata】的存在,并进行可回收性检查。此可回收性检查与之前的检查有不同之处:

  • 一些垃圾最多的区域【region】已经被收集了
  • 可能会有新添加的区域,这些区域没有在活跃度元数据【liveness metadata】

如果检查再次成功,将有另一个 Mixed 事件,它将收集剩余的包含最多垃圾的 Tenured 区域的一部分。

此时,大约 2/G1MixedGCCountTarget 的包含最多垃圾的可回收区域已被收集。此过程将持续进行,直到活跃度元数据【liveness metadata】中列出的可回收空间量小于 G1HeapWastePercent(默认5%) 设置的允许浪费量。此时,可回收性检查将失败,活跃度元数据【liveness metadata】将被丢弃,下一个 GC 事件将是 Minor 事件。除非 G1HeapWastePercent 设置为 0,否则我们预计运行的连续 Mixed 事件数量将少于目标数量。

Inherent To-space Exhaustion Vulnerability

请考虑以下显示的两个单独的 Mixed 事件循环的图示。

活跃度元数据【liveness metadata】是在 MPCMC 的尾端生成的。在这些 MPCMC 结束之间的整个期间,新的 Tenured 区域将没有元数据,并且没有资格被回收。

危险在于,在 MPCMC 之间的期间,空闲空间会通过 to-space 溢出【to-space overflow】或巨型对象【Humongous objects】而完全被 Tenured 吞没。

IHOP and the Working Set

现在我们已经了解了 Mixed 事件生命周期背后的一些背景,我们可以更深入地了解 InitiatingHeapOccupancyPercent (IHOP) 参数的影响。对于诸如 http REST API 服务器之类的应用程序,预计会有相对稳定的 Tenured 数据量。这块数据【data】或工作集【working set】可能包括但不限于以下任何内容:

  • Object caches
  • Singletons
  • Connection Pools
  • Thread Pools
  • Metrics

让我们考虑其中一个 REST API 服务器,其总堆为 1GB,工作集【working set 】为 256MB,即 25%。默认 IHOP 为 45%,Mixed 事件应在 Tenured 达到 45% 满后立即触发,因为堆中会有 25% 的工作集和 45-25 = 20% 的浪费,远高于默认的 G1HeapWastePercent 5%。Mixed 事件周期完成后,我们预计 Tenured 大小(工作集 + 浪费)约为总堆的 30%。

现在考虑一个配置类似的 REST API 实例,其工作集占总堆的 5%。混合事件在 Tenured 达到 45% 满之前不会触发。在 Tenured 的 10% 低端(5% 工作集 + 5% 允许的浪费)和 45% 的高端(最终发生混合事件时)之间有大量浪费的堆。

接下来考虑另一个配置类似的 REST API 实例,其工作集占总堆的 75%。当 IHOP 为 45% 时,如果尚未启动 MPCMC,则每个 Minor GC 事件都会触发 MPCMC。一旦 Reclaimable(即可回收检查) 达到阈值,混合事件就会开始。在这种情况下,MPCMC 几乎一直在运行,而 Mixed 事件正在疏散大量区域以回收少量空间。更糟糕的是,这些疏散比疏散大部分非实时数据的区域更昂贵。

为了有效利用资源和 STW 时间,强烈建议根据应用程序调整 IHOP。在生产中,我们的目标是将 IHOP 设定为工作集 + G1HeapWastePercent + 10-15% 左右。如果混合事件周期相隔 5 分钟,我们会降低 IHOP,如果混合事件周期相隔 5 秒,我们会提高 IHOP。

Humongous Objects

如前所述,任何单个数据分配 ≥ G1HeapRegionSize/2 都被视为 Humongous 对象,该对象从连续的可用空间区域中分配,然后添加到 Tenured。让我们了解一些基本特征以及它们如何影响正常的 GC 生命周期。以下关于 Humongous 对象的讨论将深入了解 Humongous 对象的缺点,例如:

  • 增加耗尽可用空间并触发 Full GC 的风险
  • 增加在 STW 中花费的总时间
巨型对象从可用空间池中分配

巨型对象是从可用空间中分配的。分配失败会触发 GC 事件。如果来自可用空间的分配失败触发了 GC,则 GC 事件将为 Full GC,这在大多数情况下是非常不好的。为了避免在具有大量巨型对象的应用程序中发生 Full GC 事件,必须确保可用空间池与 Eden 相比足够大,以便 Eden 始终会先填满。通常,人们会过于谨慎,最终导致应用程序处于可用内存池非常大且从未充分利用的状态,这在定义上就是在浪费内存。

巨型对象在 MPCMC 结束时被释放

在 Oracle jdk 8u45 之前,Humongous 对象确实只在 MPCMC 运行结束时收集。Oracle 8u45-8u65 版本的发布说明中有一些提交,表明在 Minor 事件期间收集了一些(但不是全部)Humongous对象。

只有在 MPCMC 结束时才可收集的巨型对象将增加对保留空闲空间的诉求,或者更有可能触发 Full GC。

巨型对象提前启动 MPCMC

为了减少未引用的 Humongous 对象所浪费的空间,当 Humongous 对象存在时,MPCMC 会更频繁地运行。每个 Humongous 对象分配都将运行 Mixed 事件部分中详述的 IHOP 检查。然而,这一次,检查不是在 Eden/From 被清除后运行的,这使得 MPCMC 更有可能被启动。如果 MPCMC 已在进行中或存在活跃度元数据【liveness metadata】,则不会运行检查。

当前使用的堆 / 总可用堆 > IHOP

如果检查通过,则立即启动 Minor 事件,无论 Eden 是 3% 还是 93% 。

此特性仅在 Minor 事件周期内有效,在 Mixed 事件周期内,MPCMC 不会由 Humongous 对象分配重新运行。因此,在 Minor 事件周期内,从未使用的 Humongous 对象中回收可用空间的速度将比在 Mixed 事件周期内更快。总的来说,Mixed 事件周期的持续时间可能会决定避免空间耗尽所需的空闲空间峰值。

防御巨型对象

有两种主要策略通常并行使用,用于防止由于巨型对象而导致的不良 GC 行为。

第一种防御方法是增加 G1HeapRegionSize,以便更少的分配符合巨型对象的条件。可以通过辅助脚本运行 GC 日志来科学地选择区域大小。但请注意,G1GC 算法针对 2k 区域进行了优化,将区域大小扩大到只有 128 个区域可能不会导致更好的行为。

如前所述,第二种防御方法是增加可用空间,以便 Eden 首先填满。创建参数 -XX:G1ReservePercent(默认值为 10)是为了允许对可用空间进行可配置的下限,但是此选项有一些注意事项,在下面"Controlling heap sizes"部分所述。

更多概念和最终想法

现在我们已经了解了 Mixed GC 和巨型对象引入的复杂性,让我们通过深入研究前面提到但留待以后讨论的几个概念来结束本文。

Controlling heap sizes

什么是理想的堆分配,以及如何配置堆?以下列表回顾了迄今为止所学到的知识,并加入了一些新事实。

  • Eden 大小不影响 Minor STW 时间(具有巨大、频繁搅动缓存的应用程序除外)
  • Tenured 由工作集 + G1HeapWastePercent 组成
  • Eden 越大,在 STW 中花费的总时间越少,注意,这个总时间是包含次数的
  • 巨型对象通过增加可用空间来缓解
  • Eden 配置为从 G1NewSizePercent(默认 5)到 G1MaxNewSizePercent(默认 60)的范围
  • InitiatingHeapOccupancyPercent(默认 45)控制 Mixed 事件的启动时间
  • G1ReservePercent(默认 10)将尝试对可用空间应用下限【low bound】

对于许多 REST API 实例以及一些其他服务(Kafka 和 ZooKeeper),理想的配置是:

  • IHOP 比工作集 + G1HeapWastePercent 高 10-15%
  • 可用空间【Free space】 10%
  • Eden 使用剩余的空间
  • MaxGCPauseMillis 比平均 Minor GC 时间高 50%
并发症 - 未达到 MaxGCPauseMillis 目标

-XX:MaxGCPauseMillis(默认值为 200)参数用于控制 Eden 的大小。如果目标时间始终得到满足,则将使用 Eden 范围的最大大小。如果很少/从未达到目标,则将使用 Eden 范围的最小值。理论上,所选的 Eden 值可能位于这个范围的中间。

在未达到 MaxGCPauseMillis 目标的情况下,可以增加 Eden 下限: G1NewSizePercent ,或增加 MaxGCPauseMillis 目标。事实证明,增加 MaxGCPauseMillis 目标是更安全的选择,因为 Eden 范围的下限不遵守 G1ReservePercent。

并发症 - G1NewSizePercent 覆盖 G1ReservePercent

选择 Eden 大小的算法大致如下:

if (recent_STW_time < MaxGCPauseMillis)

eden = min(100% - G1ReservePercent - Tenured, G1MaxNewSizePercent)

else

eden = min(100% - Tenured, G1NewSizePercent)

请注意,如果未达到 MaxGCPauseMillis(即 GC 耗时大于 MaxGCPauseMillis),则 G1ReservePercent 不是一个因素。如果目标是将尽可能多的堆专用于 Eden,请考虑以下场景。

MaxGCPauseMillis 未得到满足。Tenured 通常位于 15% 左右,IHOP 设置为 30%,Eden 范围是默认的 5-60%。15% 的 G1ReservePercent 被认为是安全的,因为偶尔会有 5% 的可用空间通过 To-space 溢出被占用。 Eden 的目标是使用所有未使用的空间,在本例中为 100% - 15% G1ReservePercent - (15-30% Tenured) = 55-70% 范围。

如果我们什么都不做,Eden 大小将为 5%,这将导致在 STW 中花费大量总时间。如果我们将最小 Eden 设置在 55-70% 的范围内,那么考虑当流量高峰时或异常大小的请求将 Tenured 提高到 40% 时堆的分布情况:

40% Tenured + 55-70% Eden = 95-110%

G1ReservePercent 之前设置为 15%,以添加足够的缓冲区来处理 5% 的常见 To-space 溢出【To-space overflow 】事件。将最小 Eden 设置为 55% 后,在一段时间内将只剩下 5% 的可用空间,大大增加了 To-space 耗尽的可能性。

并发症 - 巨型对象和/或 R.O.U.S.

如果应用程序有许多巨型对象,请增加 G1ReservePercent 设置的可用空间缓冲区。

关于 -XX:MaxGCPauseMillis 的评论

MaxGCPauseMillis 的效果比其名称所暗示的要微妙得多,主要体现在两个地方:

  1. 帮助选择每个纪元【epoch】的 Eden 大小。不满足 MaxGCPauseMillis 目标将导致使用 Eden 范围的最小端。满足目标将导致使用最大大小。实践经验表明,Eden 会设置在范围的两端,很少设置在中间。这对 STW 中花费的总时间有重大影响,因为 5% 的 Eden ,其运行 GC 的频率是堆的 60% 的 Eden 的 12 倍。
  2. 扩展在给定的 Mixed GC 事件中可以收集的 tenured 区域的数量的上限。结果可能是:剩余的 Mixed GC 事件中单个 STW 时间略长,而混合 GC 事件略少。

注释:垃圾收集生命周期的完整循环称为纪元。

设置 MaxGCPauseMillis 并不能保证所有 STW 时间都低于配置的值。

G1 GC 非常复杂,在许多用例中需要进行一些调整才能获得所需的结果。根据我们的经验,调整不当的 G1 GC 可以提供比 ParallelGC 更差的用户体验。

Java Options

XX:MaxTenuringThreshold

对象会一直驻留在 Survivor 空间中,直到它们被收集或足够老以进行提升,由 XX:MaxTenuringThreshold(默认为 15)定义。当对象达到 XX:MaxTenuringThreshold 时,它们会从 Survivor 空间中提升到老年代。

XX:G1HeapRegionSize=n

区域【region】大小是在 JVM 启动时计算和定义的。它基于尽可能接近 2048 个区域的原则,其中每个区域的大小为 2 的幂,介于 1 到 32 MB 之间。

您可以选择通过 -XX:G1HeapRegionSize 明确指定区域【region】大小。设置区域大小时,区域越少,区域就越大,G1 的灵活性就越低,扫描、标记和收集每个区域所需的时间就越长。在所有情况下,空区域都会添加到无序列表(也称为"空闲列表")中。

XX:G1ReservePercent=10

G1 GC 专为具有大内存空间(超过 4GB)的多处理器环境中的应用而设计。它从 JDK7 Update 4 开始提供。与其他收集器不同,G1 收集器将堆划分为一组大小相等的堆区域(通常为 1MB 到 32MB)块,对它们进行优先级排序,然后根据优先级对这些块执行垃圾收集。我们可以使用其他选项来调整 G1,其中之一是 G1ReservePercent。该配置用于设置保留的堆空闲内存百分比(0 到 50),以降低 G1 收集器提升失败的可能性。当您增加或减少百分比时,请确保将总 Java 堆调整相同的量。默认情况下,此选项设置为 10%。

如果你想设置为 20%,那么配置为 -XX:G1ReservePercent=20

XX:InitiatingHeapOccupancyPercent=45(IHOP)

当发生以下三件事之一时,G1 会特殊处理:

  • 它达到可配置的软边界,称为 InitiatingHeapOccupancyPercent (IHOP)。
  • 它达到其可配置的硬边界 (G1ReservePercent)
  • 它遇到巨型分配。

设置触发标记周期的 Java 堆占用率阈值。默认占用率为整个 Java 堆的 45%。

PS 此活跃率会作为每次年轻代收集的组成部分,不断地计算和评估。当其中一个触发器被触发时,会发出请求以启动并发标记周期。(这段话有待商榷)

XX:G1HeapWastePercent=5

设置您愿意浪费的堆百分比。当可回收百分比小于堆浪费百分比时,Java HotSpot VM 不会启动混合垃圾收集周期。默认值为 5%。

为了确定通过收集老年代可以回收的数据量,G1 GC 首先会创建一个可回收的老年代区域列表。该列表仅包含活跃度 ≤ -XX:G1MixedGCLiveThresholdPercent(默认值为 85)的区域,并遵循基于活跃度的自然排序(即活跃度最低的区域优先)。然后统计通过收集此列表中的区域可以回收的总字节数,并将其除以总堆以获得百分比。如果可回收百分比 ≥ -XX:G1HeapWastePercent(默认值为 5),则可回收性检查通过。

值得注意的是,老年代中可回收和不可回收的堆浪费之间存在区别。如果 20% 的堆由活跃度为 86% 的 Tenured 区域组成,那么浪费且不可回收的堆将是 .2 * .14 = .028,即约 3% 的堆。

ParallelRefProcEnabled

TODO

-XX:G1MixedGCLiveThresholdPercent=85

设置要包含在 Mixed 垃圾收集周期中的老年代区域的占用率阈值。默认占用率为 85%。这是一个实验性标记。此设置取代了 -XX:G1OldCSetRegionLiveThresholdPercent 设置。

XX:G1MixedGCCountTarget=8

设置标记周期后 Mixed 垃圾收集的目标数量,以收集活动数据最多为 G1MixedGCLIveThresholdPercent 的旧区域。默认值为 8 次 Mixed 垃圾收集。混合收集的目标是在此目标数量之内。

当你想调整老年代区域的 CSet 时,可以结合 G1MixedGCCountTarget 和 G1MixedGCLIveThresholdPercent 一起来控制。

XX:G1OldCSetRegionThresholdPercent=10

设置 Mixed 垃圾收集周期中要收集的老年代区域数量的上限。默认值为 Java 堆的 10%。

XX:MaxGCPauseMillis

设置 G1 收集过程的目标时间,默认值 200ms ,不是硬性条件。

XX:G1NewSizePercent

设置用作年轻代大小最小值的堆百分比。默认值为 Java 堆的 5%。此设置取代了 -XX:DefaultMinNewGenPercent 设置。此设置在 Java HotSpot VM build 23 中不可用。

XX:G1MaxNewSizePercent

设置用作年轻代大小最大值的堆大小百分比。默认值为 Java 堆的 60%。此设置取代了 -XX:DefaultMaxNewGenPercent 设置。此设置在 Java HotSpot VM build 23 中不可用。

XX:ParallelGCThreads

设置 STW 工作线程的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最高为 8。

XX:ConcGCThreads

设置并行标记线程数。将 n 设置为并行垃圾收集线程 (ParallelGCThreads) 数量的 1/4。

XX:GCTimeRatio=12

设置应在 GC 上花费的总目标时间与处理用户活动所花费的总时间。确定目标 GC 时间的实际公式是 [1 / (1 + GCTimeRatio)]。默认值 12 表示目标 GC 时间为 [1 / (1 + 12)],即 7.69%。这意味着 JVM 可以将 7.69% 的时间花在 GC 活动上,其余 92.3% 的时间应该花在处理用户活动上。

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端