JDK11 升级 JDK17 最全实践干货来了
1、前言
如果你仍在使用 JDK8,那你是否曾经遇到过 OutOfMemoryError 的问题?你是否曾经为 JVM 的调优问题感到困扰?本篇文章将为你介绍一种能够提供百倍性能提升的垃圾回收器,也许能够解决你的问题。
上篇文章给大家带来了JDK8升级JDK11的最全实践,相信大家阅读后已经对 JDK11 有了比较深入的了解。2021 年 9 月 14 日,Oracle 发布了可以长期支持的 JDK17 版本,那么从 JDK11 到 JDK17,到底带来了哪些特性呢?亚毫秒级的 ZGC 效果到底怎么样呢?值得我们升级吗?而且升级过程会遇到哪些问题呢?带着这些问题,本篇文章将带来完整的 JDK11 升级 JDK17 最全实践。
2、为什么升级 JDK17
1)长期支持版本
JDK17 是 Oracle 官方在 2021 年 9 月 14 日发布的一个长期支持(LTS)版本,意味着它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性。
2)性能提升
更好的垃圾回收器。综合评估,从 Java 8 升级到 Java 11,G1GC 平均速度提升 16.1%,ParallelGC 为 4.5% , 从 Java 11 升级到 Java 17,G1GC 平均速度提升 8.66%,ParallelGC 为 6.54% (基于OptaPlanner的用例基准测试表明)
最大的亮点是带来了稳定版的 ZGC 垃圾回收器,达到亚毫秒级停顿。
3)新语法和特性
Switch 表达式简化、Text Blocks 文本块、instanceof 的模式匹配升级和 NullPointerException 提示信息改进等
4)支持最新的技术和框架
Spring framework6 和 Spring Boot3 都默认使用 Java 17 作为最低版本
3、升级后压测效果
先给出结论: 1、JDK17 相对于 JDK8 和 JDK11,所有垃圾回收器的性能都有很明显的提升,特别是稳定版的 ZGC 垃圾回收器 ****2、不论任何机器配置下,都推荐使用 ZGC,ZGC 的停顿时间达到亚毫秒级,吞吐量也比较高
我在 JDOS 平台上选择了不同配置的机器(2C4G、4C8G、8C16G),并分别使用 JDK8、JDK11 和 JDK17 进行部署和压测。
整个压测过程限时 60 分钟,用 180 个虚拟用户并发请求一个接口,每次接口请求都创建 512Kb 的数据。最终产出不同 GC 回收器的各项指标数据,来分析 GC 的性能提升效果。
以下是压测的性能情况:
![](https://static001.geekbang.org/infoq/cd/cd2d40a2c36bbd9414915d78877bbbea.png)
4、OracleJDK 和 OpenJDK 的选择
2021 年 9 月,Oracle 宣布 JDK17 可以免费商用,直到下一个 LTS 版本之后继续提供整整一年,同时 Oracle 将继续按照自 Java 9 以来的相同版本和时间表提供 GPL 下的 Oracle OpenJDK 版本。
2023 年 9 月,OracleJDK 发布了新的 LTS 版本 JDK21,这就意味着从 2024 年 9 月开始,在生产环境使用 OracleJDK17 将需要付费。
![](https://static001.geekbang.org/infoq/30/305684058e619986f8e97e2af47a9538.png)
参考: https://www.oracle.com/hk/java/technologies/downloads/#java17
OracleJDK 和 OpenJDK 这两个之间没有真正的技术差别,因为针对 Oracle JDK 构建过程是基于 OpenJDK 的。自从 JDK11 开始,OracleJDK 和 OpenJDK 在功能上基本相同,所以推荐使用 OpenJDK17 或其他开源的 JDK 版本,这些开源版本都是基于 OpenJDK 构建并提供长期支持的,比如:AdoptOpenJDK、RedHatOpenJDK。
![](https://static001.geekbang.org/infoq/4a/4a3184a6ec089eaf6a061edcf78273de.png)
官方参考: https://blogs.oracle.com/java/post/oracle-jdk-releases-for-java-11-and-later
5、JDK11 到 JDK17 带来了哪些新特性
5.1、JVM 改进
1、ZGC 垃圾回收器从实验性功能更改为正式产品功能,从 JDK11 引入以来,经过持续的迭代升级,目前已经足够稳定。需要手动开启,开启方式:-XX:+UseZGC
2、G1 垃圾回收器仍然作为默认垃圾回收器,进行改进升级,主要包括可中止的混合收集集合、NUMA 可识别内存分配等
3、JDK14 开始删除 CMS 垃圾回收器
4、JDK14 开始弃用 ParallelScavenge 和 SerialOld GC 的组合使用
5、JDK15 禁用偏向锁,默认禁用:-XX:+UseBiasedLocking
6、NullPointerException 提示信息改进
JDK14 以前的出现 NullPointerException 时,只能定位到所在异常行,无法定位具体是哪个变量。改进后的 NullPointerException,可以清晰描述具体变量,提升了空指针异常的可读性。
![](https://static001.geekbang.org/infoq/e3/e3c8a4f0f5183b686dde56c8f380dac0.png)
5.2、新语法特性
5.2.1、Switch 表达式简化
switch 表达式带来了简化式的编码方式,提供了新的分支切换方式,即 -> 符号,右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常
参考: https://openjdk.org/jeps/361
传统写法
![](https://static001.geekbang.org/infoq/eb/eb948b51a308ed9a8122195779e8068e.png)
新写法
![](https://static001.geekbang.org/infoq/1c/1c092e2c5d25a5d0fbb36cfe8cca39be.png)
5.2.2、Text Blocks 文本块(5 星推荐)
参考: https://openjdk.org/jeps/378
通过编写 """,来减少转义字符和换行符,达到简化代码和提高代码可读性的目的
![](https://static001.geekbang.org/infoq/bb/bbc2ce1522ce62c07c1fd9163f008d31.png)
5.2.3、Record 类型
参考: https://openjdk.org/jeps/395
record 是 JDK 14 引入的关键字,用于声明不可变的数据类。它适用于存储纯粹的值类型数据,如接口传输数据、坐标点和只读的日志记录。与 lombok 相比,record 简化了定义纯粹数据类型的过程。由于 record 类是不可变的,成员变量只能设置一次且无法更改,无需提供显式的 setter() 方法。
1、定义 Point 类,使用关键字 record,未定义 get/set
![](https://static001.geekbang.org/infoq/b6/b601393af4ccfde6d8d591b6f438c099.png)
2、查看编译后的字节码文件
![](https://static001.geekbang.org/infoq/11/11633161e662ffb3849275f9d7bae8da.png)
![](https://static001.geekbang.org/infoq/f9/f945f8de9b8498fa3a1b1ea1af2121d1.png)
3、使用 Point 类
![](https://static001.geekbang.org/infoq/82/8297a339350707e951589e74d04f4f6f.png)
5.2.4、instanceof 的模式匹配升级
•instanceof 类型判断再也不需要强制转换
参考: https://openjdk.org/jeps/394
![](https://static001.geekbang.org/infoq/33/337f38566b8116fe9a643af8a00dc75a.png)
5.2.5、密封的类和接口
参考: https://openjdk.org/jeps/409
JDK15 开始,引入了 sealed 普通类或接口类,这些类只允许被指定的类或者 interface 进行扩展和实现。
使用修饰符 sealed,您可以将一个类声明为密封类。密封的类使用关键字 permits 列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的
比较实用的一个特性,可以用来限制类的层次结构
![](https://static001.geekbang.org/infoq/ad/ad98473a886b87099fbbc64f89971370.png)
5.2.6、其他优化和升级
感兴趣的同学,推荐阅读 OpenJDK 官方文档说明,从 JDK11 到 JDK17 的改动: https://openjdk.org/projects/jdk/17/jeps-since-jdk-11
6、升级步骤
6.1、JDK 选择
OpenJDK17 下载:https://jdk.java.net/archive/
行云镜像:jdt-base-tomcat/java-jdt-centos7.4-openjdk-17.0.2-tomcat8.0.53
6.2、pom 编译配置升级
maven 编译所需 JDK 升级至 17
6.3、SpringBoot 升级
SpringBoot 版本升级到 2.7.15,Spring 版本升级为 5.3.29
为什么不升级到 SpringBoot3?
Spring Boot 3.0 最低要求 Java 17,SpringBoot3.0 带来了很多变化,和 SpringBoot2 差异较大。 考虑到公司很多中间件都是基于 SpringBoot2 构建的,所以此处推荐升级到 SpringBoot2 的最高版本 2.7.15。
POM 升级
也可以通过设置 dependencyManagement 的方式:
参考:
spring 升级指南: https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions
springboot 版本官网: https://spring.io/projects/spring-boot#learn
循环依赖问题
SpringBoot 升级到 2.7.15 后,如果应用中存在循环依赖的问题,启动时会报如下错误:
![](https://static001.geekbang.org/infoq/de/de3413cb7a6c8cf4d40ddbc352b617ad.png)
原因: 官方文档不鼓励循环依赖引用,默认情况下是禁止的
解决方案:
第一种:推荐更新应用中 bean 的依赖关系来解决
第二种:配置文件中加入以下配置,为了和旧版本保持一致,此配置推荐添加
6.4、常用中间件升级
6.4.1、Lombok 版本升级到****1.18.20 以上
如果不升级,编译时会报错如下:
![](https://static001.geekbang.org/infoq/28/284f5480aec82a4be9872708f6eb75c4.png)
6.4.2、swgger 问题,springfox3.0.0 和 springboot2.7 版本不兼容
异常:
解决方案:
properties 配置(感谢 zhaoyongping8 验证提供):
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
参考:https://developer.aliyun.com/article/950787
6.4.3、AKS 升级(针对直接从 JDK8 升级的情况)
异常: Causedby: java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
原因: Java11 删除了 Java EE modules,其中就包括 java.xml.bind (JAXB)。
解决方案:
手动引入如下包即可
6.4.4、Concrete 配置中心阻塞升级
新版本 2.2.1 已修复此问题
使用 Concrete 时,启动时异常:
原因:
分析下 Concrete 报错的原因,如下图,包内 com.wangyin.concrete.spring.ConcreteConfigProcessor#postProcessAfterInitialization(212 行)的实现逻辑
![](https://static001.geekbang.org/infoq/fc/fc072c9857e74e9c1bca552346b24a29.png)
![](https://static001.geekbang.org/infoq/27/270f97a4027a2a5b42a693b68f24d408.png)
解决方案:
1、在 JVM 启动参数中设置--add-opens jdk.proxy2 来开启私有字段的访问,但因为动态代理生成的包名是随机不明确的,所以这种方案不可行。JDK 官方文档也明确表示不支持访问动态代理内部的随机字段。官方说明:https://cr.openjdk.org/~mr/jigsaw/spec/api/java/lang/reflect/Proxy.html
2、代码修改,只需把 f.setAccessible(true) 移到 Modifier.isStatic(f.getModifiers()) 的判断下方即可。原因是方法 Modifier.isStatic(f.getModifiers()) 本来就要跳过静态字段,这样修改直接避免了访问。推动 concrete 团队修复问题
6.5、JVM 启动参数配置
6.5.1、开启 ZGC
启动参数中配置: -XX:+UseZGC
![](https://static001.geekbang.org/infoq/8f/8fda5a144daa5892c1177238edc3d800.png)
6.5.2、不同中间件所需启动参数
升级 JDK17 后,项目启动时可能会遇到如下两种类型的异常:
1、cannot access class sun.util.calendar.ZoneInfo (in module java.base) because module java.base does not export sun.util.calendar to unnamed module @0x2611f533
2、Unable to make field final int java.math.BigInteger.signum accessible: module java.base does not "opens java.math" to unnamed module @525f1e4e
异常原因:
自从 JDK9 中引入了模块化功能后,再到 JDK17,对于包扫描和反射的权限控制更加的严格。常见的库比如(Spring)大量用到包扫描和反射,所以常出现此错误。
解决方案:
一个粗暴的解决办法是将没开放的 module 强制对外开放,即保持和 Java9 之前的版本一致。
•--add-exports 导出包,意味着其中的所有公共类型和成员都可以在编译和运行时访问。
•--add-opens 打开包,意味着其中的所有类型和成员(不仅是公共类型)都可以在运行时访问。
主要区别在于--add-opens
允许“深度反射”,即非公共成员的访问,才可以调用setAccessible(true)
SGM 需要加入:
R2M 需要加入:
Ducc 需要加入:
AKS 需要加入:
6.6、启动后的验证
1.推荐先升级 JDK11,再到 JDK17,一边升级一边进行验证观察
2.观察日志是否有异常,特别是上面说到的启动时异常
3.观察监控类软件,比如 SGM、UMP 等监控是否正常
4.推荐逐步有序切量,并做好常态化压测,防止影响核心业务
5.升级完成后,最好能做个全流程的功能测试,防止功能异常
7、总结
1、升级后,除了可以使用新的语法特性,最大的亮点是可以使用亚毫秒级停顿的 GC 性能(至少百倍的 GC 性能提升),所以 强烈建议升级到 JDK17
2、整个升级过程并不复杂,主要涉及到中间件版本的升级和启动参数的配置
如果还停留在 JDK8,推荐先升级 JDK11,再到 JDK17,具体升级步骤先参考我的上篇文章“JDK8升级JDK11最全实践干货来了”,再参考本章中的升级步骤。
希望以上分享可以给大家带来实际的帮助,升级过程中如果遇到问题,欢迎大家在评论区回复。
系列文章:
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/d6beb3264d08949244552ac24】。文章转载请联系作者。
评论