一文读懂ZGC

关于ZGC

ZGC(Z Garbage Collector)是一种可扩展的低延迟垃圾回收器,旨在满足以下目标:

  • 亚毫秒(Sub-millisencond)级的最大暂停时间
  • 暂停时间不会随着heap、live-set、root-set的增大而增加
  • 可以处理8MB到16TB的堆大小

ZGC支持:

  • 并发(Concurrent)
  • 基于Region
  • 压缩(Compacting)
  • NUMA-aware
  • 使用着色指针
  • 使用负载屏障

ZGC的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在Java线程执行的同时完成。这极大地限制了垃圾收集对应用程序响应时间的影响。

JVM如何设置

JVM一般通过JAVA_OPTS环境变量设置,如果使用Tomcat,可以使用CATALINA_OPTS设置。JAVA_OPTS与CATALINA_OPTS的不同是:

  • [JAVA_OPTS]: (optional) Java runtime options used when the “start”, “stop” or “run” command is executed
  • [CATALINA_OPTS]: (optional) Java runtime options used when the “start” or “run” command is executed

支持的平台

Platform Supported Since Comment
Linux/AArch64 支持 JDK 13
Linux/x64 支持 JDK 11
macOS 支持 JDK 14
Windows 支持 JDK 14 Requires Windows version 1803 (Windows 10 or Windows Server 2019) or later.

快速开始

如果您是第一次尝试 ZGC,请从使用以下 GC 选项开始:

-XX:+UseZGC -Xmx -Xlog:gc

如需更详细的日志记录,请使用以下选项:(在VSCode中加*,启动报错)

-XX:+UseZGC -Xmx -Xlog:gc*

示例代码:

JAVA_OPTS=”-XX:+UseZGC -Xmx1024m -Xlog:gc”

配置和调优

General GC Options

  • -XX:MinHeapSize, -Xms
  • -XX:InitialHeapSize, -Xms
  • -XX:MaxHeapSize, -Xmx
  • -XX:SoftMaxHeapSize
  • -XX:ConcGCThreads
  • -XX:ParallelGCThreads
  • -XX:UseLargePages
  • -XX:UseTransparentHugePages
  • -XX:UseNUMA
  • -XX:SoftRefLRUPolicyMSPerMB
  • -XX:AllocateHeapAt

ZGC Options

  • -XX:ZAllocationSpikeTolerance
  • -XX:ZCollectionInterval
  • -XX:ZFragmentationLimit
  • -XX:ZMarkStackSpaceLimit
  • -XX:ZProactive
  • -XX:ZUncommit
  • -XX:ZUncommitDelay

ZGC Diagnostic Options (-XX:+UnlockDiagnosticVMOptions)

  • -XX:ZStatisticsInterval
  • -XX:ZVerifyForwarding
  • -XX:ZVerifyMarking
  • -XX:ZVerifyObjects
  • -XX:ZVerifyRoots
  • -XX:ZVerifyViews

启用ZGC

使用-XX:+UseZGC参数启用ZGC。

设置Heap大小

ZGC最重要的调优选项是设置最大堆(Heap)大小 (-Xmx<size>)。由于ZGC是并发收集器,因此必须选择最大堆大小:

  1. 堆可以容纳应用程序的live-set,
  2. 在GC运行期间堆中有足够的空间分配给应用程序。

需要多少空间取决于应用程序的分配率和实时设置大小。一般来说,你给ZGC的内存越多越好。但同时,浪费内存也是不可取的,所以这一切都是为了在内存使用和GC需要运行的频率之间找到平衡。

设置并发GC线程数

第二个调优选项是设置并发 GC 线程的数量 (-XX:ConcGCThreads=<number>)。 ZGC 有启发式自动选择这个数字。这种启发式通常效果很好,但根据应用程序的特性,这可能需要进行调整。这个选项本质上决定了应该给 GC 多少 CPU 时间。给它太多,GC 会从应用程序中窃取太多 CPU 时间。给它太少,应用程序可能会比 GC 收集垃圾的速度更快地收集垃圾。

一般来说,如果低延迟(即低应用程序响应时间)对您的应用程序很重要,那么永远不要过度配置您的系统。理想情况下,您的系统的 CPU 利用率不应超过 70%。

将未使用的内存归还给操作系统

默认情况下,ZGC 不提交未使用的内存给操作系统。这对于关注内存占用的应用程序和环境很有用。可以使用 -XX:-ZUncommit禁用此功能。此外,内存不会未提交,因此堆大小会缩小到最小堆大小 (-Xms) 以下。这意味着如果最小堆大小 (-Xms) 配置为等于最大堆大小 (-Xmx),则此功能将被隐式禁用。
可以使用-XX:ZUncommitDelay=<senconds>(默认为 300 秒)配置取消提交延迟。此延迟指定内存在有资格取消提交之前应该被使用多长时间。

在 Linux 上,取消提交未使用的内存需要具有 FALLOC_FL_PUNCH_HOLE 支持的 fallocate(2),它首先出现在内核版本 3.5(用于 tmpfs)和 4.3(用于 Hugetlbfs)中。

在Linux上使用Large Pages

将 ZGC 配置为使用大页面通常会产生更好的性能(在吞吐量、延迟和启动时间方面)并且没有真正的缺点,只是设置稍微复杂一些。设置过程通常需要 root 权限,这就是默认情况下不启用它的原因。

在 Linux/x86 上,large pages(也称为“huge pages”)的大小为 2MB。

假设您想要一个 16G 的 Java 堆。这意味着您需要 16G / 2M = 8192 个大页面。
首先为大页面池分配至少 16G(8192 页)的内存。 “至少”部分很重要,因为在 JVM 中启用大页面意味着不仅 GC 将尝试将这些用于 Java 堆,而且 JVM 的其他部分将尝试将它们用于各种内部数据结构(代码堆、标记位图等)。因此,在此示例中,我们将保留 9216 个页面 (18G) 以允许 2G 的非 Java 堆分配以使用大页面。
配置系统的大页面池,使其拥有所需数量的页面(需要root权限):

$ echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

请注意,如果内核找不到足够的空闲大页面来满足请求,则不能保证上述命令会成功。另请注意,内核处理请求可能需要一些时间。在继续之前,请检查分配给池的大页面数量以确保请求成功并已完成。

$ cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
9216

如果您使用的是Linux kernel >= 4.14内核,则可以跳过下一步(您挂载 Hugetlbfs 文件系统的位置)。否则,如果您使用的是较旧的内核,则 ZGC 需要通过 Hugetlbfs 文件系统访问大页面。
挂载一个hugetlbfs 文件系统(需要root 权限)并使运行JVM 的用户可以访问它(在本例中,我们假设该用户的uid 为123)。

$ mkdir /hugepages
$ mount -t hugetlbfs -o uid=123 nodev /hugepages 

现在使用-XX:+UseLargePages选项启动 JVM。

$ java -XX:+UseZGC -Xms16G -Xmx16G -XX:+UseLargePages ...

如果有多个可访问的 Hugetlbfs 文件系统可用,那么(并且只有这样)您还必须使用-XX:AllocateHeapAt来指定要使用的文件系统的路径。例如,假设安装了多个可访问的hugetlbfs 文件系统,但您特别希望使用它的文件系统安装在/hugepages 上,然后使用以下选项。

$ java -XX:+UseZGC -Xms16G -Xmx16G -XX:+UseLargePages -XX:AllocateHeapAt=/hugepages ...

除非采取必要的措施,否则巨页池的配置和 Hugetlbfs 文件系统的安装在重新启动后会丢失。

在Linux上启用Transparent Huge Pages

使用显式大页面(如上所述)的替代方法是使用透明大页面。对于延迟敏感的应用程序,通常不推荐使用透明大页面,因为它往往会导致不必要的延迟峰值。但是,可能值得尝试一下,看看您的工作负载是否/如何受到它的影响。但请注意,您的里程可能会有所不同。

在 Linux 上,在启用透明大页面的情况下使用 ZGC 需要kernel >= 4.7

使用以下选项在 VM 中启用透明大页面:

-XX:+UseLargePages -XX:+UseTransparentHugePages

这些选项告诉 JVM 为其映射的内存发出 madvise(…, MADV_HUGEPAGE) 调用,这在 madvise 模式下使用透明大页面时很有用。
要启用透明大页面,您还需要通过启用 madvise 模式来配置内核。

$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

$ echo advise > /sys/kernel/mm/transparent_hugepage/shmem_enabled

启用NUMA支持

ZGC 具有 NUMA 支持,这意味着它会尽量将 Java 堆分配定向到 NUMA 本地内存。默认启用此功能。但是,如果 JVM 检测到它绑定到系统中的 CPU 子集,它将自动禁用。通常,您无需担心此设置,但如果您想明确覆盖 JVM 的决定,您可以使用-XX:+UseNUMA-XX:-UseNUMA选项来实现。

在 NUMA 机器(例如多路 x86 机器)上运行时,启用 NUMA 支持通常会显着提升性能。

启用GC Logging

使用以下命令行选项启用 GC 日志记录:

-Xlog:<tag set>,[<tag set>, ...]:<log file>

有关此选项的一般信息/帮助:

-Xlog:help

要启用基本日志记录(每个 GC 输出一行):

-Xlog:gc:gc.log

要启用对调优/性能分析有用的 GC 日志记录:

-Xlog:gc*:gc.log

其中 gc* 表示记录包含 gc 标记的所有标记组合,而 :gc.log 表示将日志写入名为 gc.log 的文件。