前言
自 2014 年发布以来, JDK 8 一直都是相当热门的 JDK 版本。其原因就是对底层数据结构、JVM 性能以及开发体验做了重大升级,得到了开发人员的认可。但距离 JDK 8 发布已经过去了 9 年,那么这 9 年的时间,JDK 做了哪些升级?是否有新的重大特性值得我们尝试?能否解决一些我们现在苦恼的问题?带着这份疑问,我们进行了 JDK 版本的调研与尝试。
新特性一览
现如今的 JDK 发布节奏变快,每次新出一个版本,我们就会感叹一下:我还在用 JDK 8,现在都 JDK 9、10、11 …… 21 了?然后就会瞅瞅又多了哪些新特性。有一些新特性很香,但考虑一番还是决定放弃升级。主要原因除了新增特性对我们来说改变不大以外,最重要的就是 JDK 9 带来的模块化(JEP 200),导致我们升级十分困难。
模块化的本意是将 JDK 划分为一组模块,这些模块可以在编译时、构建时和运行时组合成各种配置,主要目标是使实现更容易扩展到小型设备,提高安全性和可维护性,并提高应用程序性能。但付出的代价非常大,最直观的影响就是,一些 JDK 内部类不能访问了。
但是除此之外,并没有太多阻塞升级的问题,后续版本都是一些很香的特性:
- G1 (JEP 248、JEP 307、JEP 344、JEP 345、JEP 346),提供一个支持指定暂停时间、NUMA 感知内存分配的高性能垃圾回收器
- ZGC (JEP 333、JEP 376、JEP 377),一个支持 NUMA,暂停时间不应超过 1ms 的垃圾回收器
- 并发 API 更新(JEP 266),提供 publish-subscribe 框架,支持响应式流发布 - 订阅框架的接口,以及 CompletableFuture 的进一步完善
- 集合工厂方法(JEP 269),类似 Guava,支持快速创建有初始元素的集合
- 新版 HTTP 客户端(JEP 321),一个现代化、支持异步、WebSocket、响应式流的 JDK 内置 API
- 空指针 NPE 直接给出异常方法位置(JEP 358),以前只给代码行数,不告诉哪个方法,一行多个方法的写法一但出现空指针,全靠程序员上下文分析推理
- instanceof 的模式匹配(JEP 394),判断类型后再也不用强转了
- 数据记录类(JEP 395),一个标准的值聚合类,帮助程序员专注于对不可变数据进行建模,实现数据驱动
- Switch 表达式语法改进(JEP 361),改变 Switch 又臭又长,易于出错的现状
- 文本块(JEP 378),支持二维文本块,而不是像现在一样通过 + 号自行拼接
- 密封类(JEP 409),提供一种限制进行扩展的语法,超类应该可以被广泛访问(因为它代表了用户的重要抽象),但不能广泛扩展(因为它的子类应该仅限于作者已知的子类)
- 以及一些未提到的底层数据结构优化,JVM 性能提升……
这么多的优点,恰好能解决我们当前遇到的一些问题,因此我们决定进行 JDK 升级。
升级
升级应用评估
首先自然是要考虑要将哪些应用进行升级。我们根据以下条件进行应用筛选:
- 第一,也是最重要的一点,此系统可以通过升级,解决现有问题与瓶颈
- 第二,有完备的机制能够进行快速回归与验证,如完备的单元测试,自动化测试覆盖能力,便捷的生产压测能力等,底层的升级一定要做好完备的验证
- 第三,技术债务一定要少,不至于在升级过程中遇到一些必须解决的技术债,给升级增加难度
- 第四,负责升级的人对这个系统都很了解,除核心业务逻辑外,还能够了解引入了哪些中间件与依赖,使用了中间件的哪些功能,中间件升级后,大量不兼容的改动是否对现有系统造成影响
最终我们选取了一个结算页、收银台展示无券支付营销的应用进行升级。此应用特点如下:
- 作为核心链路的应用之一,接口响应时间要求很高,GC 是其耗时抖动的瓶颈之一
- 业务正在进行快速迭代发展,随着降本增效策略的落地,营销策略进一步精细化,营销种类、数量、范围进一步增加,给系统性能带来更大的挑战
- 日常流量不低,整点存在突发流量,并且需要承接大促流量
- 核心链路覆盖了单元测试,测试环境具备自动化回归能力,预发、生产支持常态化压测与生产流量回放
- 非 Web 应用,仅使用各个中间件的基础功能,升级出现不兼容的问题小
- 维护了 3 年,经历过多次重构,历史问题较少,几乎没有技术债务
针对以上特点,此应用很适合进行 JDK 17 升级。此应用基于 JDK 8,SpringBoot 2.0.8,除常见外部基础组件外,还使用以下公司内部中间件:UMP、SGM、DUCC、CDS、JMQ、JSF、R2M。
升级效果
可以先看下我们升级后压测的效果:
纯计算代码不再受 GC 影响
升级前
升级后
版本 | 吞吐量 | 平均耗时 | 最大耗时 |
---|---|---|---|
JDK 8 G1 | 99.966% | 35.7ms | 120ms |
JDK 17 ZGC | 99.999% | 0.0254ms | 0.106ms |
升级后吞吐量几乎不受影响(甚至提升0.01%),GC 平均耗时下降1405 倍,GC 最大耗时下降1132 倍
升级步骤
升级 JDK 编译版本
首先自然是修改 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>