Tech 导读 距离 JDK 8 发布已经过去了 9 年,那么这 9 年的时间,JDK 做了哪些升级?是否有新的重大特性值得尝试?能否解决一些现在令人苦恼的问题?带着这份疑问进行了 JDK 版本的调研与升级踩坑记录,希望本文能够帮到大家。
01
前言
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
自 2014 年发布以来, JDK 8 一直都是相当热门的 JDK 版本。其原因就是对底层数据结构、JVM 性能以及开发体验做了重大升级,得到了开发人员的认可。但距离 JDK 8 发布已经过去了 9 年,那么这 9 年的时间,JDK 做了哪些升级?是否有新的重大特性值得尝试?能否解决一些现在令人苦恼的问题?带着这份疑问进行了 JDK 版本的调研与尝试。
02
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
现如今的 JDK 发布节奏变快,每次新出一个版本,不禁会令人感叹:“我还在用 JDK 8,现在都 JDK 9、10、11 …… 21 了?”然后就会瞅瞅又多了哪些新特性。有一些新特性很香,但考虑一番还是决定放弃升级。主要原因除了新增特性对用户来说改变不大以外,最重要的就是 JDK 9 带来的模块化(JEP 200),导致升级十分困难。
模块化的本意是将 JDK 划分为一组模块,这些模块可以在编译时、构建时和运行时组合成各种配置,主要目标是使实现更容易扩展到小型设备,提高安全性和可维护性,并提高应用程序性能。但付出的代价非常大,最直观的影响就是,一些 JDK 内部类不能访问了。
但是除此之外,并没有太多阻塞升级的问题,后续版本都是一些很香的特性:
这么多的优点,恰好能解决当前遇到的一些问题,因此决定进行 JDK 升级。
03
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
3.1 升级应用评估
首先自然是要考虑要将哪些应用进行升级。根据以下条件进行应用筛选:
最终选取了一个结算页、收银台展示无券支付营销的应用进行升级。此应用特点如下:
针对以上特点,此应用很适合进行 JDK 17 升级。此应用基于 JDK 8,SpringBoot 2.0.8,除常见外部基础组件外,还使用以下公司内部中间件:UMP、SGM、DUCC、CDS、JMQ、JSF、R2M。
3.2 升级效果
可以先看下升级后压测的效果:
纯计算代码不再受 GC 影响
图1.纯计算代码不再受 GC 影响
升级前
图2.升级前示意
升级后
图3.升级后示意
版本 | 吞吐量 | 平均耗时 | 最大耗时 |
---|---|---|---|
JDK 8 G1 | 99.966% | 35.7ms | 120ms |
JDK 17 ZGC | 99.999% | 0.0254ms | 0.106ms |
升级后吞吐量几乎不受影响(甚至提升 0.01%),GC 平均耗时下降 1405 倍,GC 最大耗时下降 1132 倍。
3.3 升级步骤
首先自然是修改 maven 中指定的 JDK 版本,可以先升级到 JDK 11,同时修改 maven 编译插件。
<java.version>11</java.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-javadoc-plugin.version>3.3.2</maven-javadoc-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
然后就可以进行本地编译了,此时会暴露一些很简单的问题,比如找不到包、类等等。原因就是 JDK 11 移除了 Java EE and CORBA 的模块,需要手动引入。
<!-- JAVAX -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.0.2</version>
</dependency>
解决了编译找不到类的问题,接下来就该升级依赖的外部中间件了。对于应用来说,也就是升级 SpringBoot 的版本。支持 JDK 17 的版本是 Spring 5.3,对应 SpringBoot 2.5。
在这里建议升级至 SpringBoot 2.7,从 2.5 升级至 2.7 几乎没有需要改动的地方,同时高版本的 SprngBoot 所约定的依赖,对 JDK 17 的支持也更好。
建议进行大版本逐个升级,比如从 2.0 升级至 2.1。每升一个版本,就要仔细观察依赖版本的变化,掌握每个依赖升级的情况。SpringBoot 的升级其实意味着把所有开源组件约定版本进行大版本升级,接口弃用,破坏性兼容更新较多,需要一一鉴别。
下面以升级 Spring Boot 2.1 为例,说明升级的步骤:
至此,Spring Boot 2.1 升级完毕。接下来分析一次依赖树变化,和升级前的依赖树进行比较,查看依赖变化范围是否全部已知可控。完成后进行 Spring Boot 2.2 的升级。
以下为需要注意的升级事项,仅供参考:
spring.main.allow-bean-definition-overriding
为 true
spring.main.allow-circular-references
设置为 true
开启spring.factories
中的内容需要移动至 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件下spring-boot-properties-migrator
可以识别弃用的属性,可以考虑使用@Retention(RetentionPolicy.RUNTIME)
进行注释,以便 Spring 能够找到它们java.util.Date
和 java.util.Calendar
默认格式进行了更改,注意查看更新日志进行兼容内部中间件升级较为简单,主要是关注 JMQ、JSF 版本。其中 JSF 依赖的 Netty 和 Javassist 等都需要升级,Netty 版本较低会有内存泄漏问题。
给大家参考下升级后的依赖版本
<properties>
<!-- 基础组件版本 Start -->
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<jacoco-maven-plugin-version>0.8.10</jacoco-maven-plugin-version>
<maven-assembly-plugin-version>2.4.1</maven-assembly-plugin-version>
<maven-dependency-plugin-version>3.1.0</maven-dependency-plugin-version>
<profiles.dir>src/main/profiles</profiles.dir>
<springboot-version>2.7.13</springboot-version>
<log4j2.version>2.18.0-jdsec.rc2</log4j2.version>
<hibernate-validator.version>5.2.4.Final</hibernate-validator.version>
<collections-version>3.2.2</collections-version>
<collections4.version>4.4</collections4.version>
<netty.old.version>3.9.0.Final</netty.old.version>
<netty.version>4.1.36.Final</netty.version>
<javassist-version>3.29.2-GA</javassist-version>
<guava.version>23.0</guava.version>
<mysql-connector-java.version>5.1.29</mysql-connector-java.version>
<jmh-version>1.36</jmh-version>
<caffeine-version>3.1.6</caffeine-version>
<fastjson-version>1.2.83-jdsec.rc1</fastjson-version>
<fastjson2-version>2.0.35</fastjson2-version>
<roaringBitmap.version>0.9.44</roaringBitmap.version>
<disruptor.version>3.4.4</disruptor.version>
<jaxb-impl.version>2.3.8</jaxb-impl.version>
<jaxb-core.version>2.3.0.1</jaxb-core.version>
<activation.version>1.1.1</activation.version>
<!-- 基础组件版本 End -->
<!-- 京东中间件版本 Start -->
<ump-version>20221231.1</ump-version>
<ducc.version>1.0.20</ducc.version>
<jdcds-driver-alg-version>2.21.1</jdcds-driver-alg-version>
<jdcds-driver-version>3.8.3</jdcds-driver-version>
<jmq.version>2.3.3-RC2</jmq.version>
<jsf.version>1.7.6-HOTFIX-T2</jsf.version>
<r2m.version>3.3.4</r2m.version>
<!-- 京东中间件版本 End -->
</properties>
远程 DEBUG 参数有所变化:
JAVA_DEBUG_OPTS=" -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 "
打印 GC 日志参数的变化,在预发环境开启了日志进行观察:
JAVA_GC_LOG_OPTS=" -Xlog:gc*:file=/export/logs/gc.log:time,tid,tags:filecount=10:filesize=10m "
使用了 ZGC 的部分 JVM 参数:
JAVA_MEM_OPTS=" -server -Xmx12g -Xms12g -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=2048m -XX:+UseZGC -XX:ZAllocationSpikeTolerance=3 -XX:ParallelGCThreads=8 -XX:CICompilerCount=3 -XX:-RestrictContended -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/logs "
内部依赖需要访问 JDK 模块,如 UMP、JSF、虫洞、MyBatis、DUCC、R2M、SGM:
if [[ "$JAVA_VERSION" -ge 11 ]]; then
SGM_OPTS="${SGM_OPTS} --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED " UMP_OPT=" --add-opens java.base/sun.net.util=ALL-UNNAMED "
JSF_OPTS=" --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED"
WORMHOLE_OPT=" --add-opens java.base/sun.security.action=ALL-UNNAMED "
MB_OPTS=" --add-opens java.base/java.lang=ALL-UNNAMED "
DUC_OPT=" --add-opens java.base/java.net=ALL-UNNAMED "
R2M_OPT=" --add-opens java.base/java.time=ALL-UNNAMED "
fi
启动后完整的启动参数如下:
-javaagent:/export/package/sgm-probe-java/sgm-probe-5.9.5-product/sgm-agent-5.9.5.jar -Dsgm.server.address=http://sgm.jdfin.local -Dsgm.app.name=market-reduction-center -Dsgm.agent.sink.http.connection.requestTimeout=2000 -Dsgm.agent.sink.http.connection.connectTimeout=2000 -Dsgm.agent.sink.http.minAlive=1 -Dsgm.agent.virgo.address=10.24.216.198:8999,10.223.182.52:8999,10.25.217.95:8999 -Dsgm.agent.zone=m6 -Dsgm.agent.group=m6-discount -Dsgm.agent.tenant=jdjr -Dsgm.deployment.platform=jdt-jdos --add-opens=jdk.management/com.sun.management.internal=ALL-UNNAMED --add-opens=java.management/sun.management=ALL-UNNAMED --add-opens=java.management/java.lang.management=ALL-UNNAMED -DJDOS_DATACENTER=JXQ -Ddeploy.app.name=jdos_kj_market-reduction-center -Ddeploy.app.id=30005051 -Ddeploy.instance.id=0 -Ddeploy.instance.name=server -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Djava.util.Arrays.useLegacyMergeSort=true -Dog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -Dlog4j2.AsyncQueueFullPolicy=Discard -Xmx12g -Xms12g -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=2048m -XX:+UseZGC -XX:ZAllocationSpikeTolerance=3 -XX:ParallelGCThreads=8 -XX:CICompilerCount=3 -XX:-RestrictContended -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/logs --add-opens=java.base/sun.net.util=ALL-UNNAMED --add-opens=java.base/sun.util.calendar=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/sun.security.action=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED -Dloader.path=/export/package/jdos_kj_market-reduction-center/conf
3.4 系统验证
系统可以成功启动后,就可以进行功能验证。有几个验证重点与方法:
04
GC调优
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目
4.1 ZGC介绍
图4、5、6.ZGC介绍
如图所示,ZGC 的定位是一个最大暂停时间小于 1ms,且能够处理大小从 8MB 到 16TB 的堆,并且易于调优的垃圾回收器。ZGC 只有三个 STW 阶段,具体流程网上有大量类似文章,这里不做详细介绍。
4.2 优化方向
目前本文提到的应用日常使用 G1 约 30ms 的 GC 停顿时间,不到 1 分钟就会触发一次,大促时频率更高,暂停时间更长,导致接口性能波动较大。随着业务发展,为了优化系统大量应用了本地缓存,导致存活对象较多。ZGC 暂停时间不随堆、活动集或根集大小而增加,且极低的 GC 时间正是被需要的特性,因此决定使用 ZGC。
ZGC 作为一个现代化 GC,没有必要做过多的优化,默认配置已经可以解决 99.9% 的场景。但是应用会承接大促流量,根据观察,瞬时流量激增时 GC 时机较晚,因此应对突发流量是 ZGC 调优的一个目标,其他属性不做任何调整。
4.3 优化措施
ZGC 的一个优化措施就是足够大的堆,一般来说,给 ZGC 的内存越多越好,但也没必要浪费,通过压测观察 GC 日志,取得一个合适的值即可。只要保证:
因此,将机器升级成 8C 16G 配置,观察 GC 日志根据应用情况调整内存占用配置,最终设定为 -Xmx12g -Xms12g -XX:MaxMetaspaceSize=256m -XX:MetaspaceSize=256m -XX:MaxDirectMemorySize=2048m
,提升 ZGC 的效果。
剩下的其他优化措施则视情况而定,可以调整触发 GC 的时机,也可以改为基于固定时间间隔触发 GC。
略微提升了触发时机,-XX:ZAllocationSpikeTolerance=3
(默认为 2)应对突发流量。
CICompilerCount ParallelGCThreads
一个是提升 JIT 编译速度,一个是垃圾收集器并行阶段使用的线程数,根据实际情况略微增加,牺牲一点点 CPU 使用率,提升下效率。
另外还可以开启 Large Pages
进一步提升性能。这一步没有做,因为现在部署方式为一台物理机 Docker 混部署。开启需要修改内核,影响宿主机的其他镜像。
05
总结
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目
至此,调优完成,目前已在线上跑了一段时间,每周都有三次常态化压测,一切正常。
以上升级心得分享给大家,希望对各位有所帮助。