ZGC
介绍
ZGC(The Z Garbage Collector)是JDK 11中推出的一款追求极致低延迟的实验性质的垃圾收集器,ZGC 并发的执行代价高昂的工作,这极大的限制了垃圾收集堆应用程序的影响,使得应用线程的停顿时间不会超过 10ms,它非常适合要求低延迟的应用或者堆非常大(TB级别)的场景。
主要目标
- 亚毫秒级别的最大暂停时间
- 暂停时间不会随着堆大小、存活对象集以及 Root 集增大
- 处理的堆大小从 8M - 16TB
支持平台
系统 | jdk版本 |
---|---|
Linux/x64 | JDK 15 (Experimental since JDK 11) |
Linux/AArch64 | JDK 15 (Experimental since JDK 13) |
Linux/PowerPC | JDK 18 |
macOS/x64 | JDK 15 (Experimental since JDK 14) |
macOS/AArch64 | JDK 17 |
Windows/x64 | JDK 15 (Experimental since JDK 14) |
Windows/AArch64 | JDK 16 |
快速启动
首次使用可以使用以下参数:
-XX:+UseZGC -Xmx<size> -Xlog:gc
# 更多的gc详情
-XX:+UseZGC -Xmx<size> -Xlog:gc*
JDK 15 之前都是实验性的,可以通过命令行选项启用:
-XX: +UnlockExperimentalVMOptions -XX: +UseZGC
配置与调整
可用参数
通用参数 | ZGC专用 | ZGC诊断参数(-XX: +UnlockExperimentalVMOptions) |
---|---|---|
-XX:MinHeapSize, -Xms | -XX:ZAllocationSpikeTolerance | -XX:ZStatisticsInterval |
-XX:InitialHeapSize, -Xms | -XX:ZCollectionInterval | -XX:ZVerifyForwarding |
-XX:MaxHeapSize, -Xmx | -XX:ZFragmentationLimit | -XX:ZVerifyMarking |
-XX:SoftMaxHeapSize | -XX:ZMarkStackSpaceLimit | -XX:ZVerifyObjects |
-XX:ConcGCThreads | -XX:ZProactive | -XX:ZVerifyRoots |
-XX:ParallelGCThreads | -XX:ZUncommit | -XX:ZVer |
-XX:UseDynamicNumberOfGCThreads | -XX:ZUncommitDelay | |
-XX:UseLargePages | ||
-XX:UseTransparentHugePages | ||
-XX:UseNUMA | ||
-XX:SoftRefLRUPolicyMSPerMB | ||
-XX:AllocateHeapAt |
-Xms -Xmx
:堆的最大内存和最小内存,这里都设置为16G,程序的堆内存将保持16G不变。-XX:ReservedCodeCacheSize -XX:InitialCodeCacheSize
:设置CodeCache的大小, JIT编译的代码都放在CodeCache中,一般服务64m或128m就已经足够。-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
:启用ZGC的配置。-XX:ConcGCThreads
:并发回收垃圾的线程。默认是总核数的12.5%,8核CPU默认是1。调大后GC变快,但会占用程序运行时的CPU资源,吞吐会受到影响。-XX:ParallelGCThreads
:STW阶段使用线程数,默认是总核数的60%。-XX:ZCollectionInterval
:ZGC发生的最小时间间隔,单位秒。-XX:ZAllocationSpikeTolerance
:ZGC触发自适应算法的修正系数,默认2,数值越大,越早的触发ZGC。-XX:+UnlockDiagnosticVMOptions -XX:-ZProactive
:是否启用主动回收,默认开启,这里的配置表示关闭。-Xlog
:设置GC日志中的内容、格式、位置以及每个日志的大小。
启用ZGC
使用 -XX:+UseZGC 启用ZGC。
设置堆大小
ZGC 最重要的调优参数是 设置最大堆大小(-Xmx<size>)。由于ZGC 是一款并发收集器,因此必须设置堆大小:
1)堆能够容纳应用程序的活动集
2)足够的对空间能够让GC运行期间进行内存分配
需要多大的堆空间非常依赖分配速率和应用程序的活动集大小。一般来说,给ZGC越多内存越好。但是同样的,浪费内存是不可取的,因此需要在内存使用和GC运行之间找到一个平衡。
设置并发 GC 线程数量
第二个比较重要的参数是设置 并发GC线程数量(-XX:ConcGCThreads=<number>)。ZGC 可以自动计算一个数值。这个计算的数值通常工作的很好,但是根据应用程序的特性可能需要对此进行调试。这个选项本质上决定 GC 应该占用 CPU 多少时间。给它太多,GC 将会比应用线程占用更多的CPU时间,给它太少,应用程序产生垃圾的速度将会超过垃圾收集的速度。
注意:通常情况,低延迟(快速响应)的系统,一定不要过度使用CPU,将CPU使用率占满。理想情况下,CPU 使用率应该不超过 70%。
将不使用的内存还给操作系统
默认情况下,ZGC 会将未使用的内存返回给操作系统。这对于那些需要考虑内存占用的应用程序和环境是非常有用。这个特性可以使用 -XX:-ZUncommit 来禁用。之后,内存也不会无限制返还给操作系统,以便确保堆内存不会缩小到最小堆大小(-Xms)以下。这意味着,如果初始堆大小(-Xms)等于最大堆大小(-Xmx),这个特性将被隐式禁用。
可以用-XX:ZUncommitDelay=<seconds>(默认是300秒)来配置返还时间。 这个延迟指定了内存在多长时间内未被使用才有资格被还给操作系统。
注意:Linux/x64上的ZGC使用tmpfs或hugetlbfs文件来支持堆。这些文件使用的未提交内存需要fallocate(2)和FALLOC_FL_PUNCH_HOLE支持,FALLOC_FL_PUNCH_HOLE支持最早出现在Linux 3.5 (tmpfs)和4.3(hugetlbfs)中。在旧的Linux内核上运行时,ZGC应该像以前一样继续工作,但是禁用了uncommit功能。
在 Linux 上启用 Large Pages
配置ZGC使用 Large Pages 通常会有更好的性能(在吞吐量、延迟以及启动时间)并且没有真正的缺点,只是设置起来稍微复杂一些。设置过程中需要 root 权限,所以默认情况下没有启用。
在 Linux/86 Large Pages 也被称为 huge Pages 大小为 2MB。
假设需要一个 16G 的 JAVA 堆。这意味着你需要 16G / 2M = 8192 huge pages。
首先为 huge Pages Pool 分配至少 16G 内存。"至少"很重要,因为在 JVM 中启用 huge Pages 意味着不仅是 GC 会尝试使用这些 Java 堆,并且 JVM 的其他部分也会尝试使用,用于各种内部数据结构(code heap,marking bitmaps 等)。因此,在本例中,将保留 9216 个页面(18G)以允许 2G 非 JAVA 堆 分配来使用 huge pages。
配置系统的 huge page pool 需要的页数(需要 root 权限):
echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
注意:如果 内核 找不到足够空闲的 huge page 来满足请求,则上述命令不能保证成功。
另外注意:内核需要一定的时间处理命令。在处理之前,检查 分配给 huge page pool 的 huge page 数量,以确保命令能够正常执行。
$ cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
9216
注意: 如果使用的 Linux 内核 >= 4.14,则可以跳过下一步(挂载hugetlbfs 文件系统的位置)。 但是,如果使用的是较旧的内核,那么 ZGC 需要通过hugetlbfs 文件系统访问大页面。 挂载一个hugetlbfs 文件系统(需要root 权限)并使其可供运行JVM 的用户访问(在本例中,我们假设该用户的uid 为123)。
$ mkdir /hugepages
$ mount -t hugetlbfs -o uid=123 nodev /hugepages
启动 -XX:+UseLargePage 参数启动 JVM
$ java -XX:+UseZGC -Xms16G -Xmx16G -XX:+UseLargePages ...
如果有多个可访问的hugetlbfs 文件系统可用,那么自启动的时候需要使用 -XX:AllocateHeapAt 来指定要使用的文件系统的路径。例如,假设挂载了多个可访问的hugetlbfs 文件系统,但想使用的文件系统挂载在/hugepages 上,则使用以下选项。
$ java -XX:+UseZGC -Xms16G -Xmx16G -XX:+UseLargePages -XX:AllocateHeapAt=/hugepages ...
注意:除非采取足够的措施,否则,配置的 huge page pool 和 挂载的 hugetlbfs 文件系统不会永久生效,重启后恢复原样。
在Linux 上启用 Transparent Huge Pages
使用 显示 large pages (如上所述)的替代方法是 Transparent Huge Pages 。通常不建议对延迟敏感的应用程序使用 Transparent Huge Pages,因为它往往会导致不必要的延迟峰值。但是,可以尝试以下,看看它 如何影响 工作负载。但是请注意,所产生你的益处可能会有所不同。
-XX:+UseLargePages -XX:+UseTransparentHugePages
这些选项告诉 JVM 为它映射的内存发出 madvise(..., MADV_HUGEPAGE) 调用,这在 madvise 模式下使用透明大页面时很有用。
要启用透明大页面,您还需要通过启用 madvise 模式来配置内核。
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# and
$ echo advise > /sys/kernel/mm/transparent_hugepage/shmem_enabled
启用 NUMA 支持
ZGC 支持 NUMA,这意味着它将尽可能将 JAVA 堆分配在 NUMA 本地内存中。该特性默认启用。然而,当 JVM 检测到它只能使用单个 NUMA 的内存,该参数会被自动禁止。通常,不需要关心这个参数,但是如果想要覆盖 JVM 的设置,可以通过:
-XX:+UseNUMA or -XX:-UseNUM
在 NUMA 机器(e.g. a multi-socket x86 machine)上运行时,启用 NUMA 支持通常会显着提升性能。
启用 GC 日志
GC 日志启用使用以下命令行参数:
-Xlog:<tag set>,[<tag set>, ...]:<log file>
# 关于该参数的通用信息/help
-Xlog:help
# 启用基本日志
-Xlog:gc:gc.log
# 启用对 调优/性能分析 有用的gc 日志
-Xlog:gc*:gc.log
# 其中 gc* 表示记录所有包含 gc 标签的标签组合, :gc.log 表示将日志写入名为 gc.log 的文件中。
指针数据使用内存多重映射
内存多重映射能够将元数据位加入指针而无需更改它指向的对象。ZGC使用它实现对 染色指针。首先,这里描述如何在不使用内存多重映射或者额外硬件支持的情况下将元数据位加入指针。
没有内存多重映射的元数据位
在普通的指针中,所有位都用于描述对象的地址。两个不同位模式的两个指针指向不同的对象。一下面的指针为例:0x13210 and 0x23210. 他们指向两个不同的对象。
Hex : Binary
Address bits: 0x13210 : 0001 0011 0010 0001 0000
Address bits: 0x23210 : 0010 0011 0010 0001 0000
当向指针中添加元数据位时,不想这些位改变它指向的对象。因此,指针位被分割为两部分,一部分用于指向对象的地址,另一部分用于元数据。可以选择从哪个位置拆分,不是对于以下示例,将116个最低有效位用于对象的地址,其余位用于元数据,便构成了以下指针布局:
0xM..MAAAA : mmmm ... mmmm aaaa aaaa aaaa aaaa
(M/m = metadata, A/a = address)
使用上面示例中的两个指针的值,我们在拆分后得到以下内容:
Pointer value: 0x13210 : 0001 0011 0010 0001 0000
Metadata bits: 0x1 : 0001
Address bits: 0x3210 : 0011 0010 0001 0000
Pointer value: 0x23210 : 0010 0011 0010 0001 0000
Metadata bits: 0x2 : 0010
Address bits: 0x3210 : 0011 0010 0001 0000
0x13210 和 0x23210 两个指针指向相同的对象,地址为:0x3210。指针分别包含元数据 0x1 和 0x2。
现在已经不能通过指针 0x13210 和 0x23210 直接访问到 0x3210 地址的对象内容。首先,需要删除元数据位以获得正确的地址,然后我们才能访问指向的内存。
// 伪代码展示获取元数据位
//
// 携带元数据信息的地址
ptr_with_metadata = 0x13210;
// 移除元数据位,得到有效内存地址 0x3210
AddressBitsMask = ((1 << 16) - 1);
address = ptr_with_metadata & AddressBitsMask
use(*address)
元数据位的这种删除将 CPU 指令添加到生成的代码中,会导致应用程序变慢。
接下来让我们看一下,如何使用内存多重映射来消除在取消引用指针时删除元数据带来的影响。
使用内存多重映射的元数据位
有两个指针,并且地址位和元数据位且分开,如下图:
Pointer value: 0x13210 : 0001 0011 0010 0001 0000
Address bits: 0x3210 : 0011 0010 0001 0000
Metadata bits: 0x10000 : 0001 0000 0000 0000 0000
Pointer value: 0x23210 : 0010 0011 0010 0001 0000
Address bits: 0x3210 : 0011 0010 0001 0000
Metadata bits: 0x20000 : 0010 0000 0000 0000 0000
然后从元数据位给出的地址映射相同的 16位(64KB)开始分配内存:
+-----------+ 0x10000 -----+
| Mapping 1 | \
| | +---> +------------------------+
| | / | |
+-----------+ 0x20000 -----+ | 64 KB allocated memory |
| Mapping 2 | | |
| | +------------------------+
| |
+-----------+ 0x30000
地址位将作为分配内存的偏移量。 0x13210 和 0x23210 都将指向内存区域为 0x3210。
+-----------+ 0x10000
| |
| X | 0x13210 -----+ +----------------------+
| | \ | |
+-----------+ 0x20000 +---> | X @ offset 0x3210 |
| | / | |
| X | 0x23210 -----+ +----------------------+
| |
+-----------+ 0x30000
现在就有了与查找对象无关的属性(元数据位),访问两个指针我们将得到相同的对象。
注意
限制最大寻址内存
上面的示例显示了两个可能的元数据位值,但是可以使用的更多。对于代码使用元数据位则需要一个映射。然而,当使用内存多重映射时,使用多少元数据位存在一些限制。这个限制是因为用户进程可用的虚拟内存地址范围被限制为小于64位。在具有四级页表的Linux x64 上,用户进程 "only" 访问 128T 内存。47 个 最低有效位是可用的。其余位必须为0。这意味着将内存多重映射用于元数据位的应用程序必须在可用的 47 位 内适应元数据位和对象地址偏移。每多使用一个元数据位,可使用的内存就会减半,也可以认为是可以寻址的最大内存量。