写点什么

亚毫秒 GC 暂停到底有多香?JDK17+ZGC 初体验|得物技术

作者:得物技术
  • 2023-06-16
    上海
  • 本文字数:4611 字

    阅读完需:约 15 分钟

亚毫秒 GC 暂停到底有多香?JDK17+ZGC 初体验|得物技术

1 前言

垃圾回收器的暂停问题一直是 Java 工程师关注的重点,特别是对实时响应要求较高的服务来说,CMS 和 G1 等主流垃圾回收器的数十毫秒乃至上百毫秒的暂停时间相当致命。此外,调优门槛也相对较高,需要对垃圾回收器的内部机制有一定的了解,才能够进行有效的调优。为了解决此类问题,JDK 11 开始推出了一种低延迟垃圾回收器 ZGC。ZGC 使用了一些新技术和优化算法,可以将 GC 暂停时间控制在 10 毫秒以内,而在 JDK 17 的加持下,ZGC 的暂停时间甚至可以控制在亚毫秒级别!

2 ZGC

ZGC 相关介绍、原理,网上已经有很多类似文章,这里只做简单介绍。

2.1 设计目标

ZGC 最初在 JDK 11 中作为实验性功能引入,并在 JDK 15 中宣布为生产就绪。作为一款低延迟垃圾收集器,旨在满足以下目标:


  • 8MB 到 16TB 的堆大小支持

  • 10ms 最大 GC 暂时

  • 最糟糕的情况下吞吐量会降低 15%(低延时换吞吐量很值,吞吐量扩容即可解决)


2.2 ZGC 内存分布

ZGC 与传统的 CMS、G1 不同、它没有分代的概念,只有类似 G1 的 Region 概率,ZGC 的 Region 可以具有如下图所示的大中下三类容量:


  • 小型 Region(Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。

  • 中型 Region(Medium Region):容量固定为 32MB,用于放置大于 256KB 但是小于 4MB 的对象。

  • 大型 Region(Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中会存放一个大对象,这也预示着虽然名字叫“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB。大型 Region 在 ZGC 的实现中是不会被重分配的(重分配是 ZGC 的一种处理动作,用于复制对象的收集器阶段)因为复制大对象的代价非常高。


2.3 GC 工作过程

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,在标记、转移和重定位阶段几乎都是并发执行的,这是 ZGC 实现停顿时间小于 10ms 目标的最关键原因。



从上图中可以看出,ZGC 只有三个 STW 阶段:初始标记,再标记,初始转移。具体转移过程,网上有大量类似文章,这里不做详细介绍,大家有兴趣可以参考以下文章:


新一代垃圾回收器 ZGC 的探索与实践 ZGC 最新一代垃圾回收器 | 程序员进阶

3 为什么选择 JDK17 呢?

JDK 17 于 9 月 14 日发布,是一个长期支持(LTS)版本,这意味着它将在很多年内得到支持和更新。这也是第一个 LTS 版本,其中包含了一个可用于生产环境的 ZGC 版本。回顾一下,ZGC 的实验版本已经包含在 JDK 11(之前的 LTS 版本)中,而第一个可用于生产环境的 ZGC 版本出现在 JDK 15(一个非 LTS 版本)中。

4 升级过程

从 JDK8+G1 升级到 JDK17+ZGC,主要是在代码层面和 JVM 启动参数层面的做适配。

4.1 JDK 下载

首先 jdk17 选择的是 openjdk,下载地址:https://jdk.java.net/archive/,选择版本 17 GA


4.2 代码适配

  • JDK11 移除了 Java EE and CORBA 的模块


项目中如果用到 javax.annotation.*、javax.xml.*等等开头的包,需要手动引入对应依赖


<dependency>    <groupId>javax.annotation</groupId>    <artifactId>javax.annotation-api</artifactId></dependency><dependency>    <groupId>javax.xml.bind</groupId>    <artifactId>jaxb-api</artifactId></dependency><dependency>    <groupId>com.sun.xml.bind</groupId>    <artifactId>jaxb-core</artifactId></dependency><dependency>    <groupId>com.sun.xml.bind</groupId>    <artifactId>jaxb-impl</artifactId></dependency>
复制代码


  • maven 相关依赖版本升级


<!-- 仅供参考 --><maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version><maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version><maven-resources-plugin.version>3.2.0</maven-resources-plugin.version><maven-jar-plugin.version>3.2.0</maven-jar-plugin.version><maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version><maven-deploy-plugin.version>3.0.0-M1</maven-deploy-plugin.version><maven-release-plugin.version>3.0.0-M1</maven-release-plugin.version><maven-site-plugin.version>3.9.1</maven-site-plugin.version><maven-enforcer-plugin.version>3.0.0-M2</maven-enforcer-plugin.version><maven-project-info-reports-plugin.version>3.1.0</maven-project-info-reports-plugin.version><maven-plugin-plugin.version>3.6.1</maven-plugin-plugin.version><maven-javadoc-plugin.version>3.3.0</maven-javadoc-plugin.version><maven-source-plugin.version>3.2.1</maven-source-plugin.version><maven-jxr-plugin.version>3.0.0</maven-jxr-plugin.version>
复制代码


  • Lombok 版本升级 https://projectlombok.org/changelog


<dependency>    <groupId>org.projectlombok</groupId>    <artifactId>lombok</artifactId>   <!-- <version>1.16.20</version>-->    <version>1.18.22</version></dependency>
复制代码



  • Java9 模块化后,不允许应用程序查看来自 JDK 的所有类,会影响部分反射的运行,需要通过以下命令解决


--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
复制代码


  • 本地使用了 transmittable-thread-local-2.14.2.jar 后启动报错



在 agent 后面加上日志输出即可解决,至于原因,猜测是跟类加载顺序有关系


-javaagent:/Users/admin/Documents/transmittable-thread-local-2.14.2.jar=ttl.agent.logger:STDOUT
复制代码


以上内容仅针对彩虹桥项目升级遇到的问题,不同的业务代码适配的情况可能不一样,需要根据实际情况寻找解决方案。

4.3 JVM 参数替换

下面是一些通用 GC 参数和 ZGC 特有参数以及 ZGC 的一些诊断选型,来自官网:Main - Main - OpenJDK Wiki



具体每个参数的含义,这里不做介绍,可参考官网文档 The java Command,里面有详细说明。


JKD8+G1 的启动参数:


-server -Xms36600m -Xmx36600m-XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:+PrintReferenceGC-XX:+ParallelRefProcEnabled-XX:G1HeapRegionSize=16m-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/opt/apps/errorDump.hprof-XX:+PrintGCDetails-XX:+PrintGCDateStamps-XX:+PrintHeapAtGC-XX:+PrintGCApplicationConcurrentTime-verbose:gc-Xloggc:/opt/apps/logs/${app_name}-gc.log
复制代码


JDK17+ZGC 的启动参数如下:


-server -Xms36600m -Xmx36600m#开启ZGC-XX:+UseZGC #GC周期之间的最大间隔(单位秒)-XX:ZCollectionInterval=120#官方的解释是 ZGC 的分配尖峰容忍度,数值越大越早触发GC-XX:ZAllocationSpikeTolerance=4#关闭主动GC周期,在主动回收模式下,ZGC 会在系统空闲时自动执行垃圾回收,以减少垃圾回收在应用程序忙碌时所造成的影响。如果未指定此参数(默认情况),ZGC 会在需要时(即堆内存不足以满足分配请求时)执行垃圾回收。-XX:-ZProactive #GC日志-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M #发生OOM时dump内存日志-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/opt/apps/errorDump.hprof
复制代码

5 压测结果

直接上图





正如 ZGC 设计目标所描述,它将 GC 暂停时间从过去的几十毫秒降低到了令人惊叹的亚毫秒级别。然而,这种超低延迟表现也需要一定的代价,因为在实现低延迟的同时,ZGC 会占用一定的 CPU 资源。通常情况下,ZGC 占用的 CPU 比例不会超过 15%。在彩虹桥项目中,使用以上推荐的 JVM 参数后,ZGC 占用的 CPU 资源为 6% 左右。

6 ZGC 日志

6.1 输出 ZGC 日志

GC 日志中包含有关 GC 操作的详细信息,可以帮我们分析当前 GC 存在的问题。先来看一下上面 JVM 参数中关于 GC 日志的参数


-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M
复制代码


  • safepoint=trace:记录关于 safepoint 的 trace 级别日志。Safepoint 是 JVM 中一个特殊的状态,它用于确保所有线程在特定操作(如垃圾回收、代码优化等)之前进入安全状态。

  • classhisto*=trace:记录与类的历史相关的 trace 级别日志。age*=info:记录与对象年龄(在新生代中存在的时间)相关的 info 级别日志。

  • gc*=info:记录与垃圾回收相关的 info 级别日志。

  • file=/opt/logs/gc-%t.log:将日志写入到 /opt/logs/ 目录下的文件中,文件名为 gc-%t.log,其中 %t 是一个占位符,表示当前时间戳。

  • time,level,tid,tags:在每个日志记录中包含时间戳、日志级别、线程 ID 和标签。

  • filesize=50M:设置日志文件的大小限制为 50MB。当日志文件大小达到此限制时,JVM 将创建一个新的日志文件并继续记录。


更详细的 gc 日志配置可以参考:https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#enable-logging-with-the-jvm-unified-logging-framework

6.2 STW 关键日志

其中我们重点关注的就是 GC 的 STW 情况,以下是一些关键字代表 GC STW 阶段


  • 最基本的 STW 三阶段,初始标记:日志中 Pause Mark Start,再标记:日志中 Pause Mark End,初始转移:日志中 Pause Relocate Start。



  • 内存分配阻塞:这一般是因为垃圾生产速度大于回收速度,垃圾来不及回收,垃圾将堆占满时,线程会阻塞等待 GC 完成,关键字是 Allocation Stall(被阻塞的线程名称)



如果出现此类日志,可以尝试如下方法解决:


  1. -XX:ZCollectionInterval 该配置含义:两个 GC 周期之间的最大间隔(单位秒)。默认情况下,此选项设置为 0(禁用),可以适当调小该配置,让 GC 周期缩短、提升垃圾回收速度,但这会提升应用 CPU 占用。

  2. -XX:ZAllocationSpikeTolerance 官方的解释是 ZGC 的分配尖峰容忍度。其实就是数值越大,越早触发回收。可以适当调大该配置,更早触发回收,提升垃圾回收速度,但这会提升应用 CPU 占用。


  • 安全点:所有线程进入到安全点后才能进行 GC,ZGC 定期进入安全点判断是否需要 GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。日志关键字 safepoint ... stopped

  • dump 线程、内存:比如 jstack、jmap 命令,一般是手动 dump 导致,日志关键字 HeapDumper

7 Linux 大页内存

在 openjdk 的官网上也能看到,开启 Linux 大页内存后会提升应用的性能。



开启方式见官网文档https://wiki.openjdk.org/display/zgc/Main#Main-EnablingLargePagesOnLinux,注意除了修改系统配置外,还需要在进程 JVM 启动参数中新增-XX:+UseLargePages 配置


经过几轮压测实际测试下来,发现在开启 Linux 大页后,CPU 有 8%左右的下降,但是由于大页面会提前预留指定大小的内存,会导致机器的内存使用率较高。而且目前生产环境没有其他应用开启此配置,稳定性有待考究,生产环境自行评估是否开启。

8 总结

在本篇文章中,我们探讨了如何升级到 JDK 17,并使用最新一代垃圾回收器 ZGC。经过实践和测试,我们发现升级后的系统在垃圾回收方面表现出色,暂停时间被有效控制在 1 毫秒内。尽管这一优化过程可能会消耗额外的 CPU 资源,但所获得的超低 GC 暂停时间显然是非常值得的。总之,相比其他垃圾回收器,ZGC 的性能和稳定性已经非常优秀,而且不需要太多的调优。在大多数情况下,使用 ZGC 官方推荐的默认设置即可获得优秀的性能表现。对于那些 RT 敏感型应用,升级到 JDK 17 并采用 ZGC 是一个明智的选择。


文: 新一


本文属得物技术原创,来源于:得物技术官网


未经得物技术许可严禁转载,否则依法追究法律责任!

发布于: 刚刚阅读数: 3
用户头像

得物技术

关注

得物APP技术部 2019-11-13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
亚毫秒 GC 暂停到底有多香?JDK17+ZGC 初体验|得物技术_ZGC_得物技术_InfoQ写作社区