新一代 GC 神器 ZGC

0. 标记-复制算法

标记-复制算法分三个阶段:

  • 标记阶段:从 GC Roots 出发,标记存活对象。
  • 转移阶段:把存活对象复制到新的内存地址,原来的内存空间变成可回收的。
  • 重定位阶段:存活对象被复制到其他地方后,所有指向对象旧地址的指针都要调整到对象新的地址上。

1. 概述

Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。只支持 64 位的系统。

ZGC 未分代,每次 GC 都是 FullGC 。

1.1 ZGC 的设计目标

  • 亚毫秒级的最大暂停时间。
  • 暂停时间不随堆、存活对象数量、根集合大小的增长而增长。
  • 可处理的堆大小从 8MB 到 16TB。

总的来说,ZGC 具有如下特性:

  • 并发
  • 基于 Region
  • 压缩
  • NUMA 友好
  • 使用着色指针
  • 使用读屏障 load barriers

截至 JDK 14,ZGC 已在主流的操作系统上获得支持。

1.2 ZGC 在 JDK 各版本的改进

  • JDK 11
    • 初次引进,只支持 Linux X64 平台。
    • 不支持类卸载,-XX:+ClassUnloading 不生效。
  • JDK 12
    • 支持并发类卸载。
    • 暂停时间进一步减少。
  • JDK 13
    • 支持的堆最大尺寸从 4TB 增大到 16TB。
    • 支持 uncommitting unused memory
    • 进一步减少了 进入安全点 的时间。
    • 支持 Linux/AArch64 平台。
    • 支持参数 -XX:SoftMaxHeapSIze
  • JDK 14
    • 支持 macOS/Windows 系统。
    • 支持微、小堆,最小支持 8MB。
    • 支持 JFR 泄漏剖析。
    • 支持受限的、不连续的地址空间。
    • 支持并行 pre-touch,使用参数 -XX:+AlwaysPreTouch
    • 性能和稳定性提升。
  • JDK 15
    • 达到生产可用级别。
    • 提升 NUMA 感知。
    • 提升并发分配。
    • 支持类数据共享,Class Data Sharing, CDS。
    • Support for placing the heap on NVRAM
    • 支持类指针压缩。
    • Support for incremental uncommit
    • 修复对透明大页的支持。
    • 增加 JFR 事件。
  • JDK 16
    • 并行线程栈扫描(JEP 376)
    • 支持原位转移, in-place relocation。
    • 性能提升(分配、初始化转发表等)。

1.3 ZGC 相关参数

ZGC 相关参数比 CMS/G1 都明显少很多,更少的参数意味着更易用。

大体上可以分为三类:通用 GC 选择、ZGC 选项、ZGC 诊断选项。

1.3.1 通用 GC 选择

  • -XX:MinHeapSize, -Xms :最小堆大小
  • -XX:InitialHeapSize, -Xms :初始堆大小
  • -XX:MaxHeapSize, -Xmx :最大堆大小
  • -XX:SoftMaxHeapSize :
  • -XX:ConcGCThreads:与应用线程并发执行时的 GC 线程数量,默认为 CPU 个数的 12.5%。
  • -XX:ParallelGCThreads:STW 时 GC 线程的数量,默认为 CPU 个数的 60%。
  • -XX:UseLargePages
  • -XX:UseTransparentHugePages
  • -XX:UseNUMA
  • -XX:SoftRefLRUPolicyMSPerMB
  • -XX:AllocateHeapAt

1.3.1 ZGC 选项

  • -XX:ZAllocationSpikeTolerance:用来估算当前堆内存分配速率,在当前剩余的堆内存下,ZAllocationSpikeTolerance 越大,估算的达到 OOM 的时间越快,ZGC 就会更早第触发 GC。
  • -XX:ZCollectionInterval:ZCollectionInterval 用来指定 GC 发生的间隔,以秒为单位触发 GC 。
  • -XX:ZFragmentationLimit
  • -XX:ZMarkStackSpaceLimit
  • -XX:ZProactive
  • -XX:ZUncommit
  • -XX:ZUncommitDelay

1.3.1 ZGC 诊断选项

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

1.3.1 ZGC 参数调优

ZGC 参数一般只需调整如下几个:

  • 堆大小: -Xmx
  • GC 触发时机:ZAllocationSpikeToleranceZCollectionInterval
  • GC 线程相关:ParallelGCThreadsConcGCThreads

2. ZGC 实现的一些基础特性

2.1 着色指针

着色指针是 ZGC 的核心概念,把 GC 相关的元数据存储在对象的指针上。

Intel/AMD 64 位的处理器目前都只支持 48 位的地址空间,原因主要是现在也用不到 64 位的地址空间,硬件就没必要支持。相关的讨论可以看看 为什么64位机指针只用48个位?

但目前绝大多数 JVM 也用不到 48 位的地址空间,ZGC 就从中抠出 4 位用于存储 GC 的数据,剩下 44 位给应用使用,也就是 ZGC 支持堆最大 16TB 的由来。

因为着色指针,ZGC 不支持32位的平台,也不支持指针压缩。

着色指针的结构如下:

|(16bits)|    (1bit)   |  (1bit)  |  (1bit) |  (1bit) |(44bits,16TB address space)|
| unused | Finalizable | Remapped | Marked1 | Marked0 | Object Address           |

ZGC 在不同的标记周期、标记阶段,会改变 Remapped/Marked1/Marked0 的值。

在任意时刻,这四个标志位最多只有一个被置上。这四个标志位分别对应一个视图,因此,在任意时刻,只有一种视图是生效的。

在标记的过程中,是不会去移动对象的,但改变这几个位的值就相当于改变了对象的位置,因此需要一种手段来保证访问新的地址仍然能访问到原来的对象,这就引出了内存多重映射。

2.2 内存多重映射

内存多重映射(Multi-Mapping)将多个不同的虚拟内存地址映射到同一个物理内存地址上。图示如下,还是旧版本 4TB 的。

2.3 读屏障与指针自愈

假设在某一轮 GC 过程中使用的视图是 Mark0,在 GC 完成之后,对象 A 指向对象 B 的属性字段的值(指针)就是指向 Mark0 视图的地址空间。新一轮 GC 使用视图 Mark1,在 GC 的过程中,就需要把指向 Mark0 的指针修正为指向 Mark1 地址空间的对应值。这就是指针自愈的功能。

指针自愈的功能是通过读屏障来实现的,当从堆里读取到一个指针时,如果该指针不是指向最新的地址空间,就需要修改堆里存储的指针为正确值。

如果从堆里读取的是基本类型的值,是不需要进行自愈处理的。

Object o = obj.fieldA;          // Loading an object reference from heap
<load barrier needed here>
Object p = o;                   // no barrier, not a load from heap
o.doSomething();                // no barrier, not a load from heap
int i = obj.fieldB;             // no barrier, not an object reference

Object a = obj.x;
<load barrier needed here>      // a 的值是最新的,obj.x 的值自身也会修正
Object b = obj.x;               
<load barrier needed here>      // 直接走 FastPath

读屏障由 JIT 自动插入合适的位置,如下示例:

String n = person.name; // Loading an object reference from heap
if (n & bad_bit_mask) {
    slow_path(register_for(n), address_of(person.name));
}

3. ZGC 的 GC 算法

ZGC 的执行阶段

  1. 初始标记,STW:从 GC Roots 标记直接可达的对象。
  2. 并发标记/对象重定位:进行并发标记、并修复指向已转移对象的指针。
  3. 再标记,STW:时间不能超过 1ms,超过 1ms 重新进入并发标记阶段。
  4. 并发转移准备:引用处理、类卸载、重定位集合选择。
  5. 初始转移,STW:转移从 GC Roots 直接可达的对象。
  6. 并发转移:并发转移剩余的待转移对象。

并发转移完成后,需要修正执行被转移对象的引用,这个过程是借助下一轮 GC 标记阶段(初始、并发)来实现,下一轮 GC 周期标记完成后转发表可以清空。

ZGC 的并发标记算法也是基于 SATB (Snapshot-At-The-Begining)算法的。

4. 引起 ZGC 停顿的其他场景

  • 内存分配阻塞:当内存不足时线程会阻塞等待 GC 完成,关键字是 “Allocation Stall”。出现这种情况可以认为是堆空间偏小或 concurrent gc threads 数偏小。
  • 安全点:所有线程进入到安全点后才能进行 GC,ZGC 定期进入安全点判断是否需要 GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。
  • dump 线程、内存:jstack、jmap 命令。

5. 其他

ZGC 目前是不分代的,也就是说每次 GC 都是 Full GC。如果应用分配对象的速度远快于 GC 的进度,就可能导致可用空间用完了 GC 还没完成,导致应用阻塞。

目前的改进只能是加大堆空间或尽早开始 GC 。

6. 参考资料

The Design of ZGC
ZGC 官网


欢迎关注我的微信公众号: coderbee笔记

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据