JDK 11 升级实践

作者:大淘宝技术 查看原文open in new window

推荐语:学习java和jdk的新特性并积极应用,以达到优化系统,降本提效的作用,这是我们作为java研发同学的第一节课。本文从“为什么”起手,谈到“怎么做”,最后用数据证明“怎么样”。细致入微,深入浅出,让我获益匪浅。

——大淘宝技术开发工程师 闻尘

概要

本文以团队内部网关类应用(以下简称应用) 从 JDK 8 升级到 JDK 11 + G1 GC 的实践出发,梳理 JDK 11 升级(踩坑)指南、升级注意事项 以及 Java 9 - 19 的重要新特性:

  1. 升级收益:介绍应用升级获得的性能提升及成本收益,供大家参考。
  2. 升级指南:为大家实际动手升级时提供操作指南,避免重复踩坑。升级的主要流程和常见二方、三方依赖的升级 文章里基本都有提到。
  3. 新特性:简单介绍下 Java 9 - 19 的改进点和新增 API,主要介绍一些个人认为比较有用的新特性,看能否应用到实际开发。

为什么要升级

性能提升

通过运行 SPECJbb2015 对比分析性能,整体而言 JDK11 优于 JDK8,G1 优于 CMS。 在两个 JDK 版本默认状态下(JDK11 + G1 V.S JDK8 + CMS),JDK11 max-jOPS(纯吞吐量) 分数提升17%,critical-jOPS(限制响应时间下的吞吐量) 分数提升 105%。

注:以上数据源自内部测试,非权威数据,仅供参考。

附:本应用升级效果及成本收益

应用升级 JDK 11 + G1 GC 后,单机性能在 极限 QPS、CPU、RT、GC 表现上均有提升,落到成本上可以进一步缩减机器上百台,每年可节省数十万成本。 其中:

  1. 单机极限 QPS 提升 11% (1.8K -> 2.0K)
  2. CPU 降低 2 pt(55% -> 53%)
  3. RT 降低 5% (41ms -> 39ms)
  4. 日常水位 和 极限 QPS 下,GC 表现均有所提升

YGC 平均次数 和  平均暂停时长均降低 40-50%

极限 QPS 下 吞吐量提升4.6pt:93.99% -> 98.65%

持续跟进 Java 新版本

目前,官方已经停止 Java 8 的公共更新。作为 Java 8 后的第一个 LTS,升级 Java 11 不仅能够避免因升级跨度过大带来的稳定性风险(比如 JDK 17 中无法使用 CMS、反射依赖 JDK 内部字段或方法的代码也可能存在不兼容),而且也能降低后续升级 Java 新版本的成本。

升级指南

升级准备

常用的开发软件支持 JDK11 的最低版本:

  1. IntelliJ IDEA: 2018.2(地址:https://blog.jetbrains.com/idea/2018/06/java-11-in-intellij-idea-2018-2/)open in new window
  2. Eclipse: Photon 4.9RC2 with Java 11 plugin(地址:https://nipafx.dev/)open in new window
  3. Maven: 3.5.0

compiler plugin: 3.8.0

surefire and failsafe: 2.22.0

  1. Gradle: 5.0(地址:https://docs.gradle.org/5.0/release-notes.html#java-11-runtime-support)open in new window

基础环境升级

  1. JDK 升级,可以根据自己应用的部署模式升级

    https://openjdk.org/install/open in new window

    https://github.com/docker-library/openjdk/issues/272open in new window

  2. Tomcat 升级

    https://tomcat.apache.org/whichversion.htmlopen in new window

依赖升级

依赖检查

在 JDK8 中 JavaSE 和 JavaEE 有很多共享代码,但是 JDK11 中这两部分独立了,JavaEE 相关模块被移除,无法编译,因此需要添加包括这些包的第三方依赖。

如:编译时 @Resource/@PostConstruct等注解找不到,需要显式引入 javax.annotation

<dependency>
      <groupId>javax.annotation</groupId>
      <artifactId>javax.annotation-api</artifactId>
      <version>1.3.2</version>
    </dependency>

有一些类被移除或者变更,需要检查二方、三方依赖内是否有引用。比如 sun.misc.Cleaner 被移除。

更多被移除模块参考:

  1. JDK11删除功能和选项-阿里云开发者社区(地址:https://developer.aliyun.com/article/652709)open in new window
  2. Java 9: Removed APIs, Features, and Options(地址:https://www.oracle.com/java/technologies/javase/9-removed-features.html)open in new window

Maven升级

  1. 升级 maven 至推荐版本 3.5.0 (release)
  2. 升级 maven-compiler-plugin 到 3.8.0 以上,同时指定编译的目标文件和源文件的编译版本
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
      <source>11</source>
      <target>11</target>
    </configuration>
</plugin>

Spring升级

由于 Spring 4.x 最多只支持到 JDK 8,因此若要升级 JDK 11,建议同时升级 Spring 至 5.x 版本。

附 Spring 各版本支持的 JDK 版本范围 Spring Framework 6.0.x: JDK 17-21 (expected) Spring Framework 5.3.x: JDK 8-19 (expected) Spring Framework 5.2.x: JDK 8-15 Spring Framework 5.1.x: JDK 8-12 Spring Framework 5.0.x: JDK 8-10 Spring Framework 4.3.x: JDK 6-8

升级到 Spring 5.x 的注意点:Upgrading-to-Spring-Framework-5.x(地址:https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x)open in new window

废弃 ref local 标签

spring-beans-4.0.xsd  ref/idref 标签不再支持 local  属性,需要替换为 bean 或指定低版本的 xsd(不推荐)

The local attribute on the ref/idref element is no longer supported in the 4.0 beans XSD, since it does not provide value over a regular bean reference any more. Change your existing ref local references to ref bean when upgrading to the 4.0 schema.

参考: 官方文档(地址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-ref-element)open in new window

Spring 5.2.0 无法扫描非 Runtime 的注解

问题:线上压测时发现 某个二方库的本地缓存失效,导致下游依赖 QPS 大幅上涨。

原因:Spring 5.2.x 只能找到 @Retention(RetentionPolicy.RUNTIME) 的自定义注解,应用依赖的二方库中有非 RUNTIME 的注解,因此与 5.2.x 及以上版本不兼容。导致依赖注解扫描加载的富客户端本地缓存代理类 无法被加载,缓存失效,才会导致大量请求打到远端服务。

官方描述如下:

Spring's annotation retrieval algorithms have been completely revised for efficiency and consistency, as well as for potential optimizations through annotation presence hints (e.g. from a compile-time index). This may have side effects -- for example, finding annotations in places where they haven't been found before or not finding annotations anymore where they have previously been found accidentally. While we don't expect common Spring applications to be affected, annotation declaration accidents in application code may get uncovered when you upgrade to 5.2. For example, all annotations must now be annotated with @Retention(RetentionPolicy.RUNTIME) in order for Spring to find them. See gh-23901, gh-22886, and gh-22766.

解决:

  1. 推进二方包升级
  2. 若 1 短期内无法升级,可临时将 Spring 回退到 5.1.0,兼容二方库中非 Runtime 的自定义注解。

Log4j 兼容/升级

Spring 4.2.1 起 废弃 Log4jConfigListener,支持Apache Log4j 2,官方表述如下:

Deprecated. 
as of Spring 4.2.1, in favor of Apache Log4j 2 (following Apache's EOL declaration for log4j 1.x)


@Deprecated
public class Log4jConfigListener
extends java.lang.Object
implements ServletContextListener

两种解决方案:

  1. 升级 Log4j2(推荐)
  2. 手动使 Spring 5.x 兼容 Log4j 1.x,建议仅在依赖二方包无法平滑迁移 Log4j 2 情况下使用(不推荐)

升级 Log4j 2.x

  1. Log4j 官方提供了 bridge 包进行平滑迁移,无需代码改动。但无法兼容编程配置方式、也无法支持访问 log4j 内部实现。

  2. slf4j 版本 和 log4j-slf4j-impl 版本需要同步

    log4j-slf4j-impl should be used with SLF4J 1.7.x releases or older.

    log4j-slf4j18-impl should be used with SLF4J 1.8.x releases or newer. ref

  3. log4j 2 不支持 Servlet 2.4 ref , 需要升级到 Servlet 3.0。

具体参考参考文档:官方升级文档(地址:https://logging.apache.org/log4j/2.x/manual/migration.html)open in new window

兼容 Log4j 1.x

若依赖的二方库使用 Log4j 1.x 导致应用无法通过官方适配包平滑迁移到 Log4j2,可以自行实现 Log4jConfigListener 初始化 log4j,使 Spring 5.x 兼容 Log4j1.x

验证日志是否正确输出时需要注意,相比于 Spring 4.x,Spring 5.x 部分日志级别有所调整。

Log4jConfigListener 实现参考:(见文末附录)

  1. ASM : 7.0
  2. Guice : 4.2
  3. guava : 19.0
  4. Lombok:1.18.x
  5. Netty:需要升级到 4.1.33.Final或之后的版本,否则会引起堆外内存增长。
  6. Apache Commons Lang3:3.9

注意:

  1. 任何操作字节码的依赖都需要关注是否需要升级,比如 ASM (7.0), Byte Buddy (1.9.0), cglib (3.2.8), or Javassist (3.23.1-GA)。
  2. 从 Java 9 开始,字节码级别每六个月增加一次,因此需要定期更新相关依赖,例如:任何使用在字节码上运行的东西,如 Spring (5.1)、Hibernate (unknown)、Mockito (2.20.0) 和许多其他项

GC 升级

JDK11 在 GC 上有一些值得注意的变化,包括:

  1. 默认 GC 由 CMS 换成 G1
  2. 废弃了多种 GC 组合 和 GC 参数。
  3. 所有 GC 策略 GC log 打印出的文本格式发生了变化,和 JDK8 不兼容。

GC参数

GC log 参数改变

JDK11中打印 GC log 的参数有所变化

  1. -Xloggc:<logfile> 改为-Xlog:gc:<logfile>
  2. JDK11 中不再支持 -XX:+PrintGCDetails,PrintGCDateStamps

若使用了 -XX:+PrintGCDetails,将-Xlog:gc:<logfile>改成-Xlog:gc *:<logfile>.

若使用 -XX:+PrintGCDateStamps,在-Xlog:gc:<logfile>后面添加:time,即修改为-Xlog:gc:<logfile>:time.

废弃 GC 参数

具体废弃参数可参考:https://www.oracle.com/java/technologies/javase/9-removed-features.htmlopen in new window

配置调优参数

G1 配置和调优参数可参考:https://www.oracle.com/technical-resources/articles/java/g1gc.htmlopen in new window

ZGC 配置参数

-XX:+UnlockExperimentalVMOptions 
-XX:+UseZGC

具体调优参数参考:https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0open in new window

应用升级后单机 GC 性能压测表现

日常水位 GC 统计数据

JDK 8 + CMSJDK 11 + G1
YGC 平均次数7 次/min4 次/min**(↓43%)**
YGC 平均时长48ms25ms**(↓48%)**
吞吐率99.44%99.83%(↑0.40%)
FGC00

极限 QPS (1.8K) 统计数据

JDK 8 + CMSJDK 11 + G1
YGC 平均次数79 次/min36 次/min**(↓54%)**
YGC 平均时长45.3 ms22.3 ms**(↓51%)**
吞吐率93.99%98.65%(↑5%)
FGC 总次数 (压测过程中)20

JDK 11 + ZGC 压测时 在 400 QPS 时 RT 和 CPU 开始不正常飙高,经分析 ZGC 由于不分代,适合老年代对象较少的场景,而本应用有大量常驻内存的对象,所以不适合使用 ZGC。

升级注意事项

  1. 注意观察下游依赖监控指标。
  2. 注意观察日志,包括但不限于:中间件、Spring 启动日志;业务日志;GC 日志(格式及内容);如果有升级 日志框架,还需要看文件名格式、内容时间格式、日志滚动等是否一致。
  3. 升级 G1 GC 后,JVM 进程大小会有所增加(Remembered SetsCollection Sets 占用),需要注意应用内存变化。
Last Updated: