IT资讯 Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

mack · 2021-06-21 09:00:07 · 热度: 6

腾讯大数据 JVM 团队基于 OpenJDK11 自研的 Tencent Kona JDK11,目前已将 ZGC 特性孵化成熟,性能优于 OpenJDK 所提供的版本,使 Java 能够轻松构建响应时间在 ms 级别的强实时性在线服务,极大提高研发和运维效率,目前在腾讯内部多业务场景生产落地,实现业务延迟 SLA 提升 2-3 个数量级。

随着 2021 年 4 月 30 日 Tencent Kona JDK 11.0.10-GA 正式对外发布,生产可用的 ZGC 也正式对外开源。

背景

经过二十多年的发展,Java 语言的生态已经庞大无比,应用范围覆盖了从嵌入式设备到大型数据中心等场景,形成了各色各样的业务形态。不同的业务关注点不尽相同,如部分离线应用关注整个系统的吞吐率,而不太关注单个进程的停顿时间;另外一些应用则对于 GC 停顿的时间有严格的要求,比如以下业务形态:

  1. 在线服务交互。和用户交互的 UI 线程,需要按照特定的频率进行屏幕的刷新,比方说普通 60HZ 刷新率的屏幕,在播放动画时,需要在 1s 内刷新 60 次,才能保持屏幕画面的连续性,即需要在 15ms 内完成一次刷新,如果此时由于 GC 停顿导致 UI 线程挂起,则会导致画面出现撕裂感,最终导致用户体验的下降。

  2. 竞价广告。在竞价广告应用场景下,如 Real Time Bidding 中的广告竞价投放,一个广告栏请求播放广告时,不同的广告主则要根据当前的用户价值进行交易竞价,通常来说需要在规定的时间内(一般为 100ms 到 200ms)达成交易,否则就会错失一次广告曝光机会,此时 GC 停顿的控制就显得非常重要。

  3. 量化交易。在交易机会出现时,交易机构需要以最快的速度达成交易,对于实时性的要求就更为严苛。如果出现由于 GC 停顿造成的延迟,轻则错失交易机会,重则导致亏损。

为了满足不同的业务需求,Java 的 GC 算法也在不停迭代,对于特定的应用,选择其最适合的 GC 算法,才能更高效的帮助业务实现其业务目标。对于这些延迟敏感的应用来说,GC 停顿已经成为阻碍 Java 广泛应用的一大顽疾,需要更适合的 GC 算法以满足这些业务的需求。

近些年来,服务器的性能越来越强劲,各种应用可使用的堆内存也越来越大,常见的堆大小从 10G 到百 G 级别,部分机型甚至可以到达 TB 级别,在这类大堆应用上,传统的 GC,如 CMS、G1 的停顿时间也跟随着堆大小的增长而同步增加,即堆大小指数级增长时,停顿时间也会指数级增长。特别是当触发 Full GC 时,停顿可达分钟级别。当业务应用需要提供高服务级别协议(Service Level Agreement,SLA),例如 99.99% 的响应时间不能超过 100ms,此时 CMS、G1 等就无法满足业务的需求。

为满足当前应用对于超低停顿、高 SLA 的需求,并应对大堆和超大堆带来的挑战,伴随着 2018 年发布的 JDK 11,A Scalable Low-Latency Garbage Collector - ZGC 应运而生。腾讯大数据 JVM 团队的 Tencent Kona JDK 作为 OpenJDK Hotspot VM 下游分支,也致力于在 LTS 的 JDK11 版本上提供 Production Ready 的 ZGC 功能,满足公司内部客户的需求。

GC 停顿

1.停顿之由来

在 Hotspot 虚拟机上,GC 算法均是基于 Mark Sweep 或者 Mark Compact 实现的,也可以称为 Tracing GC。对于这类标记扫描的 GC 算法来说,需要通过 Mark 找到所有活着的对象,然后将死对象清除,或者把所有的活对象拷贝到另外一块区域,以达到清理内存的目的。因此,所有的 Tracing GC 均需要以下三个步骤,其整体过程如下面动图所示:

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

  • 找出所有的 GC Roots 集合:这是 Tracing GC 算法的起点,GC Roots 主要为运行时的关键数据结构中存放的指向堆对象的指针,如线程栈上的堆对象指针等。

  • 标记过程:从 GC Roots 开始遍历整个对象图,找出所有存货的对象。而剩余未被标记的对象则为死对象。

  • 清理过程:将死掉的对象清理掉,释放其占用的内存。当然清理时可以直接释放对象内存,也可以将所有的活对象移动到一块连续的区域里,并将原来的内存空间释放。

可以看到,上面三个步骤均需要一致的信息,如 GC Roots 需要是完整的,不能在扫描时被随意修改;标记过程,需要扫描到所有活着的对象,其他线程不能随意修改对象图;清理过程如果需要搬移对象,则需要更新所有指向该对象的地方,如对象 A 指向 B,B 被搬动之后,A 中的指针需要同步更新。因此要求在这三步中,采取同步措施,而最简单的同步措施就是暂停所有的 Java 线程,即 Stop-The-World(STW),在 STW 期间,GC 线程就可以安全的访问各种运行时数据、对象图、更新对象指针等。如果需要降低 STW 的时间,则需要将 GC 的不同阶段的任务移出 STW,和 Java 线程进行并发执行,这个时候就需要算法和数据结构方面的更改,以满足 GC 线程和 Java 线程对当前 GC 的一致性。

不同的 GC 算法实现,其 STW 阶段需要完成的任务大相径庭,造成不同 GC 算法 STW 时长的不同。所有的任务均在 STW 阶段完成时,这类 GC 就不需要和应用线程抢占 CPU,从应用整体来看,最终的吞吐率是比较高的,如 Parallel GC;而当 STW 阶段的任务减少时,则需要在并发阶段增加相应的任务——即部分 GC 任务需要和业务线程一起运行,相互抢占 CPU,这类 GC 根据不同的任务划分,最终在吞吐率和停顿之间达到一个平衡,如 CMS 和 G1 致力于以较小的吞吐率损失换取较小的停顿和较高的响应;ZGC 和 ShenandoahGC 则关注极致停顿,尽一切可能减少 STW 的工作量,从而实现 ms 级别的停顿时间。

2.业界解决方案

为了与 Apple 的 iOS 系统进行竞争,Google 主导的 Android 系统需要解决的一大问题就是显示卡顿问题,通过对 GC 算法的不断演进,实现基于 Baker Barrier 的 Concurrent Copy GC 算法,停顿时间控制在几个 ms 级别,小于 15ms 的刷新约束,补全了 Java 在嵌入式设备中的短板,使得松散的 Java 生态能够实现和严格控制的 Apple 生态一样流畅的系统。

在 ZGC 和 ShenandoahGC 出现之前,Hotspot JVM 的 GC 停顿很难平稳的控制在百毫秒以下,极大地阻碍了 OpenJDK 在金融行业等延迟敏感的场景的应用。但是作为 OpenJDK 的下游分支,Azul 的 Zing 虚拟机凭借其闭源的 C4 GC,实现了近乎“无停顿”的低延迟,在前十几年中大放异彩,频繁出现在各类交易系统中。云上的各类系统和普通的桌面应用,则面临着无低延迟 GC 可用的窘境。为满足业务需求,一般会采用 C++ 等 Native 语言重写一些重要模块,如腾讯的广告系统采用 C++ 实现,或者购置 Zing 等实现低延迟 GC 的 VM,抑或在 CMS 和 G1 上八仙过海、各显神通,利用经验调整参数来满足基本的业务需求。

Hotspot 上的停顿,导致备选方案面临 C++ 的高开发门槛、经费成本和调参经验等各种问题,以及大堆管理问题,使得停顿成为 Java 开发者心中的梦魇,阻碍了 Java 在低延迟需求业务的应用,此乃当前 Hotspot JVM 上的停顿之殇、开发者之痛。为了解决这一问题,ZGC 采用了和 Azul 的 Zing VM 相似的 GC 算法,从 JDK11 开始开源孵化,直到 JDK15 补全各类功能,成为真正可以商用的正式版本,保证了 Java 停顿时间不会随着堆大小和业务规模的增加而增长。

JDK11 在 2018 年下半年发布,是最新的 Long-Term Support 版本,而后续 LTS 版本为 JDK17,将于 2021 年下半年发布,JDK12 到 JDK16 属于中间过渡开发版本,不会像 JDK11 和 JDK17 一样提供持续的更新和修复。ZGC 在 OpenJDK11 上属于 Experimental 实验特性,无法满足业务的商用需求,腾讯 JVM 团队为了提前满足业务的需求,在 Tencent Kona JDK11 持续的更新和修复的同时,将 ZGC 的各项功能补全,并进行了长期的验证落地,使得 Tencent Kona JDK11 上的 ZGC 能够达到商用水平,让停顿敏感的业务应用在 JDK11 这个 LTS 版本上实现超低 GC 延迟。

ZGC 简介

ZGC 是由提议 JEP 333(https://openjdk.java.net/jeps/333)引入 Hotspot Runtime,其目标是为了彻底解决 GC 停顿带来的延迟问题,总的设计目标为:

  • 每次 GC 总的停顿时间控制在 10ms 以下

  • 相对于 G1,应用的吞吐率降低不超过 15%

  • 支持大堆和特大堆(8MB~16TB),并且停顿时间不随堆大小的增长而增长

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

由设计目标可知,ZGC 主要是为现在及未来大堆的管理问题服务,致力于以最小的性能损失换取最大的停顿优势。从 Oracle 发布的测试数据来看 (参见 [1]),上图中 SPECjbb2015 上 ZGC 的吞吐率(max-JOPS)和 Parallel GC、G1GC 相差无几,而体现停顿影响的指标 critical-JOPS 则提升了 20%+;在暂停时间上,ZGC 则不会超过 10ms,而 Parallel GC 和 G1GC 则高达 100ms+,如下图所示。因此,ZGC 尤其适合对延迟比较敏感的大堆任务。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

1. ZGC算法实现

为减少停顿,需要减少 STW 中执行的任务,ZGC 主要在以下三个方面进行推进:

  • GC Roots 的扫描。将能够移除到 STW 以外的 Roots 扫描外移到并发阶段,Roots 扫描的并发外移需要对 Roots 的数据结构进行改造,以支持 GC 线程和 Java 线程同时操作。

  • Runtime 数据结构的处理。在 Runtime 中维护了很多张表来记录 Meta(class、method、jit code 等),并且 Java 存在一类特殊的弱引用,即 java.lang.ref.Reference 及其子类,需要额外处理。

  • 对象移动的并发化改造。为了能够让移动对象和 Java 线程同时运行,需要增加 Read barrier 来保证每次对象 field 读取的正确性。

ZGC 在 GC 算法的处理逻辑上有很大的变更,但是在整体逻辑上,与其前辈 GC 算法一样,都是 Mark&Compact 形式。具体实现上,ZGC 下面六个阶段通过来实现低延迟的 GC 算法,如下图所示:

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

  • 第一个阶段是 Pause Mark Start:主要做一些全局状态的设置和全局数据结构的初始化这类轻量化的任务,标明后续并发阶段需要做 GC 的 Concurrent Mark。

  • 第二个阶段是 Concurrent Mark & Remap:将耗时占比最大的 GC Roots 进行并发化改造,支持并发 Roots 标记。从 GC Roots 进行对象图的并发标记。上一轮 GC 的指针更新(Remap)通过 Piggyback,放到当前阶段执行,从而减少对对象图的遍历。

  • 第三个阶段是 Pause Mark End:这一阶段做 Concurrent Mark 的同步,结束并发标记阶段,同时设置部分全局变量。

  • 第四个阶段是 Concurrent Prepare:这一阶段主要做 java.lang.ref.Reference 等弱引用的处理,并选择出需要 Compact 的 ZGC Region。

  • 第五个阶段是 Pause Relocate Start:这一阶段和第三阶段比较类似,主要是全局同步,设置全局变量,并指示 Relocate 阶段的开始。

  • 第六个阶段是 Concurrent Relocate:并发的搬移对象。

相对于其他 GC,ZGC 需要三个 STW 阶段来做全局的同步,但每个 STW 中的任务都很明确,需要完成的任务的时间和 CPU 的处理速度正相关,因此可以做到 ms 级别的停顿。相对于 G1GC,ZGC 的难点在于如何进行 GC Roots 的并发化改造和对象搬移的并发化改造。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

对于对象搬迁的并发化改造,ZGC 则采用 Colored Pointer 来实现轻量级的 Read Barrier,如上图所示。对于 64bit 的系统,高位 bit 中拿出 4 个 bit 来指示不同的处理状态,两个 Mark 位表明该对象指针是否已经被标记,采用两个 Mark bit 可以在前后不同的 GC 时使用不同的 Mark bit;Remapped 位表示当前对象指针是否已经调整为搬移之后的对象指针;Finalizable 位主要是为 Finalizable 对象服务,用来表示该对象指针是否仅经 Finalize 对象标记,主要供 Mark 阶段和弱引用处理阶段使用。通过 Colored 指针,不同的 GC 阶段,当前 Runtime 的正确的指针颜色仅为一种颜色 (Marked 或者 Remapped),就可以通过下图所示,测试对象指针是否为 bad color 即可,在 x86 上最终实现为一条 test 指令和一条 jne 跳转指令。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

Colored Pointer 导致不同的时期,对象的指针的高位是不同,如下图中的对象指针 0x0000000012345678,在程序运行过程中,可能以下面三种状态被 Java 线程感知到:Remapped 状态、Mark1 状态、Mark0 状态。为了使得这几种不同的状态(不同值的指针),指向同一份对象,ZGC 完全利用了操作系统的虚拟地址和物理地址转换,使得这三种状态的虚拟地址指针指向同一份物理地址,因此 ZGC 的 Java 堆需要在虚拟地址中占用三份地址。ZGC 通过内存文件来占用实际的物理内存,然后将这个内存文件映射到 Remapped、Mark0 和 Mark1 指向的虚拟地址。可以看出,虽然表面上 ZGC 的 Java Heap 占用了三份虚拟地址,但是实际的物理地址只有一份。这也是 linux 的命令 top 或者 ps 看到启用 ZGC 的 Java 进程 RSS 内存膨胀三倍的原因,但开启 ZGC 之后观察到的 RSS 消耗并非实际物理内存消耗。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

2. ZGC算法的开销

ZGC 对于业务线程的影响主要集中在以下五个方面:

  • Read barrier 的开销。在 Java 程序中,对象指针的读取次数要远超于对象指针的写入次数,Read Barrier 的插入点要远多于 Write Barrier 的插入点,因此 ZGC 的 Read Barrier 会对程序的性能产生较大的负面影响。

  • JIT 方法的 entry barrier 开销。如果 JIT 之后的代码包含了已经死掉的 java 对象,那么该方法就应该丢弃掉,因此 JIT 的代码需要在进入时利用一个 entry barrier 来保证自身和其包含的 meta 信息的有效性。ZGC 对每个 JIT 代码都生成 nmethod entry barrier,会对 JIT 方法产生轻微的性能损失。

  • Frame barrier 开销。为并发进行 Java 栈帧的扫描,降低 Stack Roots 扫描对 STW 时间的影响,当前 Hotspot 采用 StackWaterMark 来进行并发扫栈。同时为了降低业务线程扫描栈帧的工作量,Hotspot 中采用单个栈帧扫描的方式,即在回栈时如果超过当前 stack water mark,就会陷入 stack mark barrier,修复 caller 的 java 对象指针。参见 https://openjdk.java.net/jeps/376

  • 其他 Runtime 改造产生的锁结构带来的开销。

  • ZGC 中大部分的 GC 工作放在并发阶段,因此并发阶段 GC 线程和 Java 业务线程抢占 CPU,导致的对业务线程的抢占开销。

可以看出 ZGC 为了降低 STW 造成的停顿影响,采取的措施是极致的并发化改造,也就是以轻微的性能损失换取最低的停顿影响。当前最新的 ZGC 实现停顿已经达到 ms 级别,低于 Linux 内核的背景噪声,即调度开销和系统调用开销,也有可能造成 10ms 级别的影响,可以说 ZGC 使得 Java 不能服务实时业务的古板印象得到彻底的颠覆。

ZGC 的使用和调参

1. ZGC典型应用场景

此之蜜糖、彼之砒霜,不同的 GC 算法都有其长短处,ZGC 出现的最大优势是能够在保证停顿时间控制 10ms 以下,但为了实现这种高 SLA 的停顿时间,其代价是性能的损失和内存消耗。从前面介绍可以看出,为了降低 STW 中的工作,很多 GC 任务做了并发化改造,而并发化改造的代价则散乱在各种运行细节中,通过整个 OpenJDK 社区的持续投入,当前 ZGC 在性能损失场景中的性能下降已经控制在很小的范围内。对于性能来说,不同的配置对性能的影响是不同的,如充足的内存下即大堆场景,ZGC 在各类 Benchmark 中能够超过 G1 大约 5% 到 20%,而在小堆情况下,则要低于 G1 大约 10%;不同的配置对于应用的影响不尽相同,开发者需要根据使用场景来合理判断。当前 ZGC 不支持压缩指针和分代 GC,其内存占用相对于 G1 来说要稍大,在小堆情况下较为明显,而在大堆情况下,这些多占用的内存则显得不那么突出。因此,以下两类应用强烈建议使用 ZGC 来提升业务体验:

  • 超大堆应用。超大堆(百 G 以上)下,CMS 或者 G1 如果发生 Full GC,停顿会在分钟级别,可能会造成业务的终端,强烈推荐使用 ZGC。

  • 高 SLA 需求的应用。如对响应时间有 P999 时限要求的实时和软实时应用,此类应用无论堆大小,均推荐采用低停顿的 ZGC。

2. ZGC参数设置

ZGC 之美不仅在于其超低的 STW 停顿,也在于其参数的简单,绝大部分生产场景都可以自适应。当然,极端情况下,还是有可能需要对 ZGC 个别参数做个调整,大致可以分为三类:

  • 堆大小:Xmx。ZGC 能够通过极致的低延迟满足业务高标准 SLA 的服务准入条件,但是与所有编程语言的 concurrent GC 类似,延迟是以内存空间作为 trade-off 的。当分配速率过高,超过回收速率,造成堆内存不够时,会触发 Allocation Stall,这类 Stall 会减缓当前的用户线程。因此,当我们在 GC 日志中看到 Allocation Stall,通常可以认为堆空间偏小或者 concurrent gc threads 数偏小。

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

  • GC 线程:ParallelGCThreads, ConcGCThreads。ParallelGCThreads 是设置 STW 任务的 GC 线程数目,默认为 CPU 个数的 60%;ConcGCThreads 是并发阶段 GC 线程的数目,默认为 CPU 个数的 12.5%。增加 GC 线程数目,可以加快 GC 完成任务,减少各个阶段的时间,但也会增加 CPU 的抢占开销,可根据生产情况调整。

由上可以看出 ZGC 需要调整的参数十分简单,通常设置 Xmx 即可满足业务的需求,大大减轻 Java 开发者的负担。当前 Tencent Kona JDK11 上开启 ZGC 的参数为:“-XX:+UnlockExperimentalVMOptions -XX:+UseZGC”。

ZGC 生产注意事项

1.  RSS 内存异常现象

由前面 ZGC 原理可知,ZGC 采用多映射 multi-mapping 的方法实现了三份虚拟内存指向同一份物理内存。而 Linux 统计进程 RSS 内存占用的算法是比较脆弱的,这种多映射的方式并没有考虑完整,因此根据当前 Linux 采用大页和小页时,其统计的开启 ZGC 的 Java 进程的内存表现是不同的。在内核使用小页的 Linux 版本上,这种三映射的同一块物理内存会被 linux 的 RSS 占用算法统计 3 次,因此通常可以看到使用 ZGC 的 Java 进程的 RSS 内存膨胀了三倍左右,但是实际占用只有统计数据的三分之一,会对运维或者其他业务造成一定的困扰。而在内核使用大页的 Linux 版本上,这部分三映射的物理内存则会统计到 hugetlbfs inode 上,而不是当前 Java 进程上。

2. 共享内存调整

ZGC 需要在 share memory 中建立一个内存文件来作为实际物理内存占用,因此当要使用的 Java 的堆大小大于 /dev/shm 的大小时,需要对 /dev/shm 的大小进行调整。通常来说,命令如下(下面是将 /dev/shm 调整为 64G):

vi /etc/fstabtmpfs /dev/shm tmpfs defaults,size=65536M 0 0

首先修改 fstab 中 shm 配置的大小,size 的值根据需求进行修改,然后再进行 shm 的 mount 和 umount。

umount /dev/shmmount /dev/shm

3. mmap 节点上限调整

ZGC 的堆申请和传统的 GC 有所不同,需要占用的 memory mapping 数目更多,即每个 ZPage 需要 mmap 映射三次,这样系统中仅 Java Heap 所占用的 mmap 个数为 (Xmx / zpage_size) * 3,默认情况下 zpage_size 的大小为 2M。

为了给 JNI 等 native 模块中的 mmap 映射数目留出空间,内存映射的数目应该调整为 (Xmx / zpage_size)3*1.2。

默认的系统 memory mapping 数目由文件 /proc/sys/vm/max_map_count 指定,通常数目为 65536,当给 JVM 配置一个很大的堆时,需要调整该文件的配置,使得其大于 (Xmx / zpage_size)3*1.2。

ZGC在腾讯大规模的生产与实践

目前 Tencent Kona JDK11 的 ZGC 已经在腾讯广告大数据场景,腾讯云 VPC、WAF 等业务场景上长期稳定运行,并协助业务取得了优异的性能表现。

1. 支持广告海量数据查询

Hermes 是腾讯自研的大数据实时分析系统,具有海量数据实时接入和存储、低延迟查询分析的特性,支持千级维度的多维分析,以及日增量万亿的海量日志接入和查询分析。在广告业务实时 OLAP 分析业务中,要求 Hermes 系统上 99% 的 SQL 查询端到端延迟不超过 3s,而采用默认配置的 G1 GC 时仅 98.1% 的 SQL 查询端到端延迟不超过 3s。通过切换 Kona 11 ZGC,SQL 端到端延迟满足率上升为 99.5%,同时单个 SQL 查询中 GC 造成的延迟不超过 20ms。

2. 超大堆支持

腾讯 VPC 团队为腾讯云提供网络控制服务,该服务主要存储用于云上资源通信的网络配置等信息,并提供配置信息的查询、修改和下发等服务。业务要求在 512G 内存的机器上尽可能多的存储配置信息(最大支持 800M 的监听数目),并且保证压力场景下读写延迟不超过 1s。采用 G1GC 则会高频率出现大量的延迟超过 10s,通过腾讯大数据 JVM 团队配合切换 ZGC,并解决 ZGC 在业务遇到的 Mark Stack Overflow、进入 Safepoint 缓慢、ZGC Mark 假死等问题,最终使得业务能够在压力场景下,将预期的业务存储容量提升 12.5%,同时读写延迟不超过 50ms。

3.  助力提升 SLA

腾讯 WAF 团队采用 Java 来快速实现产品功能迭代及上线,其中,旁路安全服务是一个基于 Netty 框架的 Http 服务,此服务对时延要求很严格,需要达到 99.99% 端到端请求时延小于 80ms 的 SLA 目标。因此 GC 的 STW 对此服务有一定负面影响,需要进一步降低“世界暂停”时间。在使用 ZGC 之前,WAF 团队使用的是 G1GC,前期花费了大量时间对 G1 GC 进行选项调试,并进行了代码层面的修改。但由于 G1GC 本身的不足,仍然存在请求抖动延迟,无法达到既定的 SLA 目标。在腾讯大数据 JVM 团队的配合下,切换 ZGC 之后,该业务的 P9999 请求延迟稳定小于 80ms,为用户提供了更快速、稳定的服务。

社区回馈

腾讯大数据 JVM 团队在支持业务切换 ZGC 的同时,将遇到的相关问题和修复积极向社区报告和回馈,争做 OpenJDK 社区好公民。

1. ZGC 与 VectorAPI 联合使用问题

在广告某业务中,上线 VectorAPI 以提升机器学习效率,同时打开 ZGC 以满足服务 SLA,在业务运行过程中出现结果非预期现象,并且社区存在类似的错误报告。通过对 JIT 生产的汇编代码进行分析,发现存在 load barrier 缺失现象。经分析,在 C2 的 Vector 优化阶段需要对 Vector 节点进行 Unbox 操作,该优化阶段会新生成一个 load 节点,并且又未考虑 GC Barrier 对 load 操作的影响,即 ZGC 需要对 load 操作生成 load barrier,从而导致这个新生成的 load 节点缺少 load barrier 信息,最终未能生成相关 barrier 指令。通过对 Vector 优化阶段新生产的 load 操作增加 GC barrier 处理流程,使得该阶段能够生成带 gc barrier 信息的 load 节点,从而在不同 GC 选项下均能生成对应正确的 barrier 代码。该修复贡献给社区后以 P2 优先级合入 JDK16。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

2. ZGC Mark Stack Overflow 问题

腾讯云某业务对 ZGC 进行灰度时,出现 JVM 进程崩溃现象,相关日志显示是由于 ZGC 标记阶段使用的 Mark Stack 超过预先设定的 8G 内存导致,而通常情况下 Mark Stack 的使用不会超过 32M。经过业务全力配合,拿到一个可复现的场景进行深入分析,发现在这种场景下 ZGC 对于 Mark Stack 的使用存在两个缺陷:第一,大量的 Mark Stack 未使用满就塞入全局队列,造成单个 Stack 内存碎片问题;第二,大量的对象被多次压入 Mark Stack 中,造成 Stack 中的 Entry 重复率很高,浪费 Stack 空间。腾讯大数据 JVM 团队作出快速修复,验证 Mark Stack Overflow 问题可解后,将该问题和修复报告 OpenJDK 社区,社区基于提交的 patch 给出了更为优雅的修复方案,并将腾讯大数据 JVM 团队作为 co-author 联合提交代码入库,目前两个问题的修复均已入库 JDK17。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

3.ZGC Mark 假死问题

在分析 ZGC 在业务上的表现时,需要打开 gc debug log 选项,在启动后不久,出现进程卡死现象。分析发现绝大部分 gc worker 线程处在“Concurrent Mark Try Terminate”阶段,并且在等待“Concurrent Mark Idle”阶段的 log 文件读写锁,另外一个 gc worker 线程处于写 log 过程中,由此可以分析出由于 gc worker 线程均在抢 log 文件锁,导致 gc worker 线程最终形成一种动态死锁状态,即所有的 gc worker 线程均处于“等锁 ->拿锁 ->释放锁”这种无限循环中。这种假死现象是由于 ZGC 的 Concurrent Mark 退出机制导致的,在退出机制中所有的 gc worker 线程会等待 1ms 来进行状态同步,而等待结束后会进行相关 log 打印,这个打印需要前述 log 文件锁,从而导致动态假死现象出现。腾讯大数据 JVM 团队快速修复该问题,并提交社区,目前该贡献已合入 JDK17 中。

Tencent Kona JDK11 无暂停内存管理 ZGC 生产实践

Tencent Kona JDK 开源

腾讯大数据 JVM 团队的 Tencent Kona JDK 最新版本已经正式对外发布,大家可以使用 Tencent Kona JDK 11.0.10-GA 享受到 ZGC 带来的好处。

Tencent Kona JDK 8.0.5-GA 同步更新 OpenJDK 8u282ga

https://github.com/Tencent/TencentKona-8

Tencent Kona JDK 11.0.10-GA 同步更新 OpenJDK 11.0.10-ga

https://github.com/Tencent/TencentKona-11

猜你喜欢:
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册