ZGC(Z Garbage Collector)是Java平台上的一种垃圾收集器,它是由Oracle开发的,旨在解决大堆的低延迟垃圾收集问题。ZGC是一种并发的分代垃圾收集器,它主要针对具有大内存需求和低停顿时间要求的应用程序
分代ZGC收集器具备以下特性:
- 没有多重映射内存
- 内存屏障优化
- 双重缓冲记忆集
- 无需额外堆内存重分配
- 堆区域密度
- 大对象处理
核心概念
染色指针
染色指针是指向堆中对象的指针,该对象与对象的内存地址一起包含对对象的已知状态进行编码的元数据。元数据描述了对象是否已知是活动的、地址是否正确等等。
在分代 ZGC 中,存储在对象字段中的对象引用被实现为染色指针。然而,存储在 JVM 堆栈中的对象引用在硬件堆栈或 CPU 寄存器中实现为无色指针,没有元数据位。读屏障和存储屏障控制染色指针和无色指针之间的转换。
由于染色指针永远不会出现在硬件堆栈或 CPU 寄存器中,因此只要可以有效地完成染色指针和无色指针之间的转换,就可以使用更奇特的染色指针布局
Generational ZGC 使用的染色指针布局将元数据放在指针的低位中,将对象地址放在高位中,这最大限度地减少了负载屏障中的机器指令数量。通过仔细编码内存地址和元数据位,单个移位指令(在 x64上)既可以检查指针是否需要处理,也可以删除元数据位。
GC 阶段标记
非分代 ZGC 判断指针处于哪一个GC阶段很简单,只需要简单的位移:
movq rax, 0x10(rbx)
testq rax, 0x20(r15)
jnz slow_path
testq
即等价于&
操作,是一般的 bitflag 做法
分代 ZGC 的代码是这样的:
movq rax, 0x10(rbx)
shrq rax, $address_shift
ja slow_path
shrq
是右移操作,同时会设置 Carry Flag 为最后移除的一位,同时如果右移的结果为 0,Zero Flag 也会被设为 0。
ja
是 jump if above 指令,仅在CF == 0 && ZF == 0
时跳转
该指令的操作过程可以见下图
每次加载均会将地址右移,同时由于 8 字节对齐,JVM 保证了最低三位的值一定为 0,因此若该指针被更新(最后被移除的位值为 1),则会跳入 slow path 分支处理下一个 GC 阶段
最大堆大小
对于64位系统,ZGC支持最大堆大小:JDK11(4TB) -> JDK15(16TB) -> JDK21(16TB+)
在64位的Linux操作系统中,高18位(或称为高16TB)是由内核保留的,在用户空间是无法直接寻址的。JDK15中使用了其中两位作为标志位
HotSpot虚拟机的标记实现方案
- Serial: 标记记录在对象头上
- G1/Shenandoah: 标记记录在与对象相互独立的数据结构(BitMap)上
- ZGC: 标记信息记在引用对象的指针上
多重映射内存
分代ZGC不再使用多重映射内存
内存多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上,是一种多对一映射
内存屏障
由于分代 ZGC 的元数据比较多,使用多重映射内存的方法不再能行得通。因此,在寄存器和栈中的内存地址需要为普通的无色指针。分代 ZGC 不再能通过此减少加载或存储内存屏障的开销,需要在有色和无色指针之间转换,即:
- 加载屏障: 在加载时移除元数据
- 存储屏障: 在存储时恢复元数据
用于优化屏障的一些技术是:
- 快路径和慢路径
- 最小化加载屏障职责
- 记忆集屏障
- SATB 标记屏障
- 混合存储屏障检查
- 存储屏障缓冲区
- 屏障修补
快路径和慢路径
快路径检测是否需要额外的 GC 工作,当需要时,会跳转进入慢路径,开始相关工作。快路径由 JIT 实现,会直接插入 GC 代码至 JIT 编译后的程序。而慢路径不经常调用,所以使用 C++ 实现
最小化加载屏障职责
分代 ZGC 中,我们需要监控年轻代和老年代,并且在有色指针和无色指针间转换。为了简化加载屏障的复杂性,并引入优化加载屏障的空间,标记的职责交给了加载屏障
在分代 ZGC 中,加载屏障负责:
- 转换有色指针为无色指针
- 更新已被 GC 更新的过时指针
存储屏障负责:
- 转换无色指针为有色指针
- 维护记忆集
- 标记对象存活
记忆集和 SATB
记忆集和SATB的概念与G1中一致,详细可见G1 垃圾收集器详解
存储屏障缓冲区
将障碍分为快速路径和慢速路径,并使用指针着色,可以减少对 C++ 慢速路径函数的调用次数。
分代 ZGC 通过在快速路径和慢速路径之间放置 JIT 编译的中间路径来进一步减少开销。中间路径将要覆盖的值和对象字段的地址存储在存储屏障缓冲区中,并返回到已编译的应用程序代码,而不需要采取昂贵的慢速路径。仅当存储屏障缓冲区已满时才采用慢速路径。这可以分摊从编译的应用程序代码转换到 C++ 慢路径代码的一些开销
双重缓冲记忆集
ZGC 的记忆集不使用卡表实现,而是由两个 bitmap 实现。一个 bitmap 用于用户线程,在加载屏障中修改,另一个只读的 bitmap 用于 GC。这样做有两个好处:
- 用户线程无需等待 bitmap 被清除
- 因为分了两个 bitmap,所以不需要额外的内存屏障,造成额外的内存开销
无需多余堆空间的重分配
其他 HotSpot GC 中的年轻代回收使用清理模型,GC 一次性找到存活对象并重分配。在 GC 完全了解哪些对象还活着之前,年轻代中的所有对象都必须重分配,在这之后才能回收内存。因此,这些 GC 需要猜测存活对象所需的内存量,并确保在 GC 启动时该内存量可用。如果猜错了,则需要更昂贵的清理操作:例如,就地固定未重分配的对象,这会导致内存碎片,或者 Full GC。
分代 ZGC 有两个阶段:
- 访问并标记所有可达对象
- 重分配标记的对象
由于 GC 在重分配之前就知道对象是否存活,因此可以按区域粒度划分工作。一旦存活对象都被重分配出某个区域,即该区域已被清除,该区域就被当作新的目标区域,继续用于重分配或被应用使用。即使没有额外的堆空间,ZGC 仍可通过将压缩对象到当前区域来继续重分配。这使得分代 ZGC 能够重分配并压缩年轻代,而无需使用额外的堆内存
堆区域密度
如果一个区域的存活对象很多,将它们一个个移到老年代堆的操作是不值得的。ZGC 会分析年轻代存活对象的密度,以此为一句来判断是否有机会就地升级为老年代。否则,这个区域会保留为年轻代
大对象处理
ZGC 已经可以很好地处理大型对象。通过将虚拟内存与物理内存解耦,并提前保留虚拟内存,大对象的碎片问题通常可以避免
在分代 ZGC 中,允许在年轻代中分配大对象。鉴于该区域现在可以在不重分配的情况下老化,因此不再需要在老一代中分配大对象。相反,如果大对象寿命较短,则可以在年轻代中收集它们;如果寿命较长,则可以廉价地将它们提升到老年代。
ZGC JVM参数
ZGC 通用参数
参数 | 描述 | 默认值 |
---|---|---|
-XX:MinHeapSize, -Xms | 最小堆大小 | 8M |
-XX:InitialHeapSize, -Xms | 初始化堆大小 | 128M |
-XX:MaxHeapSize, -Xmx | 最大堆大小 | 2036M |
-XX:SoftMaxHeapSize | JVM堆的最大软限制 | 2036M |
-XX:ConcGCThreads | 并发GC的线程数量 | 1 |
-XX:ParallelGCThreads | 设置垃圾回收时的并行GC线程数量 | 4 |
-XX:UseLargePages | 使用大页面内存 | false |
-XX:UseTransparentHugePages | 使用Transparent大页面内存 | |
-XX:UseNUMA | 使用UNMA内存分配,可以获得更好的性能 | |
-XX:SoftRefLRUPolicyMSPerMB | 每MB的空闲内存空间允许软引用对象存活时间 | 1000 |
-XX:AllocateHeapAt | 堆分配参数,可以使用非DRAM 内存 |
ZGC 特有参数
参数 | 描述 | 默认值 |
---|---|---|
-XX:ZAllocationSpikeTolerance | 修正系数,数值越大,越早触发GC | 2.000000 |
-XX:ZCollectionInterval | ZGC发生的最小时间间隔,单位秒 | 0.000000 |
-XX:ZFragmentationLimit | relocation时,当前region碎片化大于此值,则回收region | 25.000000 |
-XX:ZMarkStackSpaceLimit | 指定为标记堆栈分配的最大字节数 | 8096M |
-XX:ZProactive | 是否启用主动回收 | true |
-XX:ZUncommit | 是否归还不使用的内存给OS | true |
-XX:ZUncommitDelay | 不再使用的内存最多延迟多久会归还给OS | 300s |
ZGC 诊断选项
通过
-XX:+UnlockDiagnosticVMOptions
开启诊断选项
参数 | 描述 |
---|---|
-XX:+UnlockDiagnosticVMOptions | 使用诊断模式,下面的参数才会起作用 |
-XX:ZStatisticsInterval | 指定统计数据输出之间的时间间隔(秒) |
-XX:ZVerifyForwarding | 检验转发表 |
-XX:ZVerifyMarking | 检验标记集 |
-XX:ZVerifyObjects | 检验对象 |
-XX:ZVerifyRoots | 检验根节点 |
-XX:ZVerifyViews | 检验堆视图访问 |
分代ZGC 特有参数
参数 | 描述 |
---|---|
-XX:ZCollectionIntervalMinor | ZGC进行年轻代垃圾收集(MinorGC)的时间间隔(秒) |
-XX:ZCollectionIntervalMajor | ZGC进行老年代垃圾收集(MajorGC)的时间间隔(秒) |
-XX:ZYoungCompactionLimit | 控制ZGC何时进行年轻代的压缩操作 |
参考资料:
- 深入理解Java虚拟机第三版
- JEP 439: Generational ZGC
- JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)
- 理解并应用JVM垃圾收集器-ZGC
- 分代ZGC
- G1 垃圾收集器详解
- JDK17+ZGC初体验|得物技术