> Linux新闻 >

JDK11 升级 JDK17 最全实践干货来了 | 京东云技术团队

1、前言

上篇文章给大家带来了 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 的性能提升效果。

以下是压测的性能情况:

4、OracleJDK 和 OpenJDK 的选择

2021 年 9 月,Oracle 宣布 JDK17 可以免费商用,直到下一个 LTS 版本之后继续提供整整一年,同时 Oracle 将继续按照自 Java 9 以来的相同版本和时间表提供 GPL 下的 Oracle OpenJDK 版本。

2023 年 9 月,OracleJDK 发布了新的 LTS 版本 JDK21,这就意味着从 2024 年 9 月开始,在生产环境使用 OracleJDK17 将需要付费。

参考: https://www.oracle.com/hk/java/technologies/downloads/#java17

OracleJDK 和 OpenJDK 这两个之间没有真正的技术差别,因为针对 Oracle JDK 构建过程是基于 OpenJDK 的。自从 JDK11 开始,OracleJDK 和 OpenJDK 在功能上基本相同,所以推荐使用 OpenJDK17 或其他开源的 JDK 版本,这些开源版本都是基于 OpenJDK 构建并提供长期支持的,比如:AdoptOpenJDK、RedHatOpenJDK。

官方参考: 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,可以清晰描述具体变量,提升了空指针异常的可读性。

5.2、新语法特性

5.2.1、Switch 表达式简化

switch 表达式带来了简化式的编码方式,提供了新的分支切换方式,即 -> 符号,右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常

参考: https://openjdk.org/jeps/361

传统写法

新写法

5.2.2、Text Blocks 文本块

参考: https://openjdk.org/jeps/378

通过编写 """,来减少转义字符和换行符,达到简化代码和提高代码可读性的目的

5.2.3、Record 类型

参考: https://openjdk.org/jeps/395

record 是 JDK 14 引入的关键字,用于声明不可变的数据类。它适用于存储纯粹的值类型数据,如接口传输数据、坐标点和只读的日志记录。与 lombok 相比,record 简化了定义纯粹数据类型的过程。由于 record 类是不可变的,成员变量只能设置一次且无法更改,无需提供显式的 setter () 方法。

1、定义 Point 类,使用关键字 record,未定义 get/set

2、查看编译后的字节码文件

3、使用 Point 类

5.2.4、instanceof 的模式匹配升级

  • instanceof 类型判断再也不需要强制转换

参考: https://openjdk.org/jeps/394

5.2.5、密封的类和接口

参考: https://openjdk.org/jeps/409

JDK15 开始,引入了 sealed 普通类或接口类,这些类只允许被指定的类或者 interface 进行扩展和实现。

使用修饰符 sealed,您可以将一个类声明为密封类。密封的类使用关键字 permits 列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的

比较实用的一个特性,可以用来限制类的层次结构

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

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

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 升级

<parent>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-parent</artifactId>
 <version>2.7.15</version>
</parent>

也可以通过设置 dependencyManagement 的方式:

<properties>
    <!-- 框架版本配置-->
    <springboot-version>2.7.15</springboot-version>
    <springframework.version>5.3.29</springframework.version>
</properties>  

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>${springboot-version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-framework-bom</artifactId>
            <version>${springframework.version}</version>
            <scope>import</scope>
            <type>pom</type>
        </dependency>
    </dependencies>
</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 后,如果应用中存在循环依赖的问题,启动时会报如下错误:

原因:官方文档不鼓励循环依赖引用,默认情况下是禁止的

解决方案:

第一种:推荐更新应用中 bean 的依赖关系来解决

第二种:配置文件中加入以下配置,为了和旧版本保持一致,此配置推荐添加

#放开循环依赖
spring.main.allow-circular-references=true

6.4、常用中间件升级

6.4.1、Lombok 版本升级到 1.18.20 以上

<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>1.18.20</version>
</dependency>

如果不升级,编译时会报错如下:

6.4.2、swgger 问题,springfox3.0.0 和 springboot2.7 版本不兼容

异常:

Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: 
Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getPatterns()" because "this.condition" is null

解决方案:

/**
 * 增加如下配置可解决Spring Boot 2.7.15 与Swagger 3.0.0 不兼容问题
 **/
@Bean
public BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {

@Override
 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
            }
return bean;
}

private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
            List<T> copy = mappings.stream().filter(mapping -> mapping.getPatternParser() == null).collect(Collectors.toList());
            mappings.clear();
            mappings.addAll(copy);
        }

@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
                Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                field.setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
            } catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
            }
        }
    };
}

参考: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)。

解决方案:

手动引入如下包即可

<!-- API, java.xml.bind module --> 
<dependency>
      <groupId>jakarta.xml.bind</groupId>
      <artifactId>jakarta.xml.bind-api</artifactId>
      <version>2.3.2</version>
</dependency> 
<!-- Runtime, com.sun.xml.bind module -->
<dependency>
       <groupId>org.glassfish.jaxb</groupId>
       <artifactId>jaxb-runtime</artifactId>
       <version>2.3.2</version>
</dependency>

6.4.4、Concrete 配置中心阻塞升级

使用 Concrete 时,启动时异常:

 Unable to make field private static final java.lang.reflect.Method jdk.proxy2.$Proxy97.m0 accessible: 
 module jdk.proxy2 does not "opens jdk.proxy2" to unnamed module @61d47554

原因:

分析下 Concrete 报错的原因,如下图,包内 com.wangyin.concrete.spring.ConcreteConfigProcessor#postProcessAfterInitialization(212 行)的实现逻辑

解决方案:

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 团队修复问题或更换使用 Ducc 配置中心

6.5、JVM 启动参数配置

6.5.1、开启 ZGC

启动参数中配置:-XX:+UseZGC

移除 - XX:ConcGCThreads,行云部署下 JVM 参数配置需要清除

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)

参考: https://stackoverflow.com/questions/44056405/whats-the-difference-between-add-exports-and-add-opens-in-java-9

SGM 需要加入:

--add-opens java.management/java.lang.management=ALL-UNNAMED 
--add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED 
--add-opens java.management/sun.management=ALL-UNNAMED

R2M 需要加入:

--add-opens java.base/java.time=ALL-UNNAMED

Ducc 需要加入:

--add-opens java.base/java.util.concurrent=ALL-UNNAMED
--add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED
--add-opens java.base/java.security=ALL-UNNAMED
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED
--add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED 
--add-opens java.base/java.net=ALL-UNNAMED 
--add-opens java.base/sun.nio.ch=ALL-UNNAMED 

AKS 需要加入:

--add-exports java.base/sun.security.action=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/sun.util.calendar=ALL-UNNAMED

6.6、启动后的验证

1. 推荐先升级 JDK11,再到 JDK17,一边升级一边进行验证观察

2. 观察日志是否有异常,特别是上面说到的启动时异常

3. 观察监控类软件,比如 SGM、UMP 等监控是否正常

4. 推荐逐步有序切量,并做好常态化压测,防止影响核心业务

5. 升级完成后,最好能做个全流程的功能测试,防止功能异常

7、总结

1、升级后,除了可以使用新的语法特性,最大的亮点是可以使用亚毫秒级停顿的 GC 性能(至少百倍的 GC 性能提升),所以 强烈建议升级到 JDK17

2、整个升级过程并不复杂,主要涉及到中间件版本的升级和启动参数的配置

如果还停留在 JDK8,推荐先升级 JDK11,再到 JDK17,具体升级步骤先参考我的上篇文章 “JDK8 升级 JDK11 最全实践干货来了”,再参考本章中的升级步骤。

希望以上分享可以给大家带来实际的帮助,升级过程中如果遇到问题,欢迎大家在评论区回复。

作者:京东科技  曲振富

来源:京东云开发者社区 转载请注明来源

(责任编辑:IT)