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…
6.2 STW 要害日志
其中咱们重点关注的就是 GC 的 STW 状况,以下是一些关键字代表 GC STW 阶段
- 最根本的 STW 三阶段,初始标记:日志中 Pause Mark Start,再标记:日志中 Pause Mark End,初始转移:日志中 Pause Relocate Start。
- 内存调配阻塞:这个别是因为垃圾生产速度大于回收速度,垃圾来不及回收,垃圾将堆占满时,线程会阻塞期待 GC 实现,关键字是 Allocation Stall(被阻塞的线程名称)
如果呈现此类日志,能够尝试如下办法解决:
- -XX:ZCollectionInterval 该配置含意:两个 GC 周期之间的最大距离(单位秒)。默认状况下,此选项设置为 0(禁用),能够适当调小该配置,让 GC 周期缩短、晋升垃圾回收速度,但这会晋升利用 CPU 占用。
- -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-EnablingLargeP…,留神除了批改系统配置外,还须要在过程 JVM 启动参数中新增 -XX:+UseLargePages 配置
通过几轮压测理论测试下来,发现在开启 Linux 大页后,CPU 有 8% 左右的降落,然而因为大页面会提前预留指定大小的内存,会导致机器的内存使用率较高。而且目前生产环境没有其余利用开启此配置,稳定性有待讲究,生产环境自行评估是否开启。
8 总结
在本篇文章中,咱们探讨了如何降级到 JDK 17,并应用最新一代垃圾回收器 ZGC。通过实际和测试,咱们发现降级后的零碎在垃圾回收方面表现出色,暂停工夫被无效管制在 1 毫秒内。只管这一优化过程可能会耗费额定的 CPU 资源,但所取得的超低 GC 暂停工夫显然是十分值得的。总之,相比其余垃圾回收器,ZGC 的性能和稳定性曾经十分优良,而且不须要太多的调优。在大多数状况下,应用 ZGC 官网举荐的默认设置即可取得优良的性能体现。对于那些 RT 敏感型利用,降级到 JDK 17 并采纳 ZGC 是一个理智的抉择。
文:新一
本文属得物技术原创,来源于:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!