跳到主要内容

JEP 346:立即从 G1 返回未使用的提交内存

概括

增强G1垃圾收集器,以在空闲时自动将Java堆内存返回给操作系统。

非目标

  • 在 Java 进程之间共享已提交但空的页面。内存应返回(未提交)给操作系统。

  • 回馈内存的过程不需要节约CPU资源,也不需要是瞬时的。

  • 使用不同的方法返回内存,而不是可用的未提交内存。

  • 支持 G1 以外的其他收集器。

成功指标

如果应用程序活动非常低,G1 应在合理的时间内释放未使用的 Java 堆内存。

动机

目前,G1 垃圾收集器可能无法及时将已提交的 Java 堆内存返回给操作系统。 G1 仅在完整 GC 或并发周期期间从 Java 堆返回内存。由于G1努力完全避免full GC,并且仅根据Java堆占用和分配活动触发并发循环,因此在许多情况下它不会返回Java堆内存,除非外部强制这样做。

这种行为在资源按使用付费的容器环境中尤其不利。即使在 VM 由于不活动而仅使用分配的内存资源的一小部分的阶段,G1 也将保留所有 Java 堆。这导致客户一直为所有资源付费,而云提供商无法充分利用他们的硬件

如果 VM 能够检测 Java 堆未充分利用的阶段(“空闲”阶段),并在此期间自动减少其堆使用量,那么两者都会受益。

Shenandoah和 OpenJ9 的GenCon 收集器已经提供了类似的功能。

对Bruno 等人第 5.5 节中的原型进行的测试表明,根据 Tomcat 服务器在白天提供 HTTP 请求且在夜间大部分时间处于空闲状态的实际利用率,该解决方案可以减少内存量85% 由 Java VM 提交。

描述

为了实现向操作系统返回最大内存量的目标,G1 将在应用程序不活动期间定期尝试继续或触发并发周期以确定总体 Java 堆使用情况。这将导致它自动将 Java 堆中未使用的部分返回给操作系统。或者,在用户控制下,可以执行完整 GC 以最大化返回的内存量。

如果满足以下条件,则应用程序被视为不活动,并且 G1 会触发定期垃圾收集:

  • 自任何先前的垃圾收集暂停以来已经过去了超过G1PeriodicGCInterval几毫秒,并且此时没有正在进行的并发循环。值为零表示禁用定期垃圾收集以立即回收内存。

  • JVM 主机系统(例如容器)上的调用返回的平均一分钟系统负载值getloadavg()如下G1PeriodicGCSystemLoadThreshold。如果G1PeriodicGCSystemLoadThreshold为零,则忽略此条件。

如果不满足这些条件中的任何一个,则取消当前的预期定期垃圾收集。下次时间G1PeriodicGCInterval过去时,会重新考虑定期垃圾收集。

定期垃圾收集的类型由选项的值决定G1PeriodicGCInvokesConcurrent:如果设置,G1 继续或启动并发循环,否则 G1 执行完整 GC。在任一收集结束时,G1 都会调整当前的 Java 堆大小,从而可能将内存返回给操作系统。新的Java堆大小由现有的用于调整Java堆大小的配置确定,包括但不限于MinHeapFreeRatioMaxHeapFreeRatio、 以及最小和最大堆大小配置。

默认情况下,G1 在此定期垃圾收集期间启动或继续并发循环。这最大限度地减少了应用程序的中断,但与完整集合相比,最终可能无法返回尽可能多的内存。

此机制触发的任何垃圾收集都会标有G1 Periodic Collection原因。此类日志的示例如下:

(1) [6.084s][debug][gc,periodic ] Checking for periodic GC.
[6.086s][info ][gc ] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms
(2) [9.087s][debug][gc,periodic ] Checking for periodic GC.
[9.088s][info ][gc ] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms
(3) [12.089s][debug][gc,periodic ] Checking for periodic GC.
[12.091s][info ][gc ] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms
(4) [15.092s][debug][gc,periodic ] Checking for periodic GC.
[15.097s][info ][gc ] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms
(5) [18.098s][debug][gc,periodic ] Checking for periodic GC.
[18.100s][info ][gc ] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms
(6) [21.101s][debug][gc,periodic ] Checking for periodic GC.
[21.102s][info ][gc ] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms
(7) [24.104s][debug][gc,periodic ] Checking for periodic GC.
[24.104s][info ][gc ] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms

在上面的示例中,以 3000ms 运行,在步骤 (1) 中,在应用程序处于非活动状态后,G1PeriodicGCIntervalG1 启动并发周期,如(Concurrent Start)和所示。(G1 Periodic Collection)此并发循环最初返回一些内存,如容量数量的减少(78M)以及(32M)从 (1) 到 (2) 所示。在(2)到(4)之间的时间间隔内,会触发更多定期收集,这次触发混合收集以压缩堆。下面的周期性垃圾收集(5)到(7)启动一个并发周期,因为 G1 策略确定此时老年代中没有足够的垃圾来启动混合 GC 阶段。在这种情况下,定期垃圾收集 (5) 到 (7) 将不会进一步缩小堆,因为已经达到最小堆大小。

在应用程序不活动期间(例如,由于软引用过期)对对象活动性的更改可能会触发该空闲时间期间提交的 Java 堆的进一步减少。

备择方案

类似的功能可以从VM外部实现,例如,通过jcmd注入VM的工具或一些代码。这具有隐藏成本:假设使用基于 cron 的任务执行检查,如果节点上有数百或数千个容器,这可能意味着堆压缩操作由许多容器同时执行,这导致主机上的 CPU 峰值非常大。

另一种选择是自动附加到每个 Java 进程的 Java 代理。然后,当容器在不同时间启动时,检查时间会自然分布,而且由于不需要启动任何新进程,因此 CPU 成本较低。然而,这种方法给用户增加了很大的复杂性,这可能会阻碍采用。

给定的用例(及时缩小 Java 堆)被认为是一个相当常见的用例,需要在 VM 中提供特殊支持。

风险和假设

在配置的默认值中,我们禁用此功能。这不会导致延迟或吞吐量敏感应用程序的虚拟机行为发生意外变化。启用后,我们假设通常将 Java 堆内存归还给操作系统是可取的,并且由此产生的并发周期或其延续对应用程序吞吐量的影响可以忽略不计。

启用此功能后,VM 将在上述条件下运行这些定期收集,而不管其他选项如何。例如,VM 可以假设用户是否设置-Xms-Xmx其他(组合)选项以获得最小且一致的垃圾收集暂停。出于一致性原因,情况并非如此。

如果定期垃圾收集仍然过多地干扰程序执行,我们提供控制来让决策考虑整个系统 CPU 负载,或者让用户完全禁用定期垃圾收集。