将JVM从JDK11迁移到JDK16的问题 - reputation


我们的后端网络服务运行在Java SE 11(JDK11)上。JDK11有很多现代化的功能,得到了Oracle和OpenJDK开发团队的长期支持,而且一直非常非常稳定,只有一个例外。内存尖峰管理。

我们有一个数据密集型的ETL进程,每天晚上运行。它从我们的数据科学团队的系统中把我们新计算出来的声誉分数加载到MongoDB集合中,以便于实时查询。一个Kubernetes实例对所有实例处理的工作进行排队,使ETL工作并行。我们的客户自然在同一时间安排了一些数据密集型的Reputation Score报告。这造成了内存激增,JDK11的垃圾收集器除了分配更多的内存外,无法处理。堆经常增长,超过了虚拟机上的可用内存,这导致Kubernetes回收排队的ELT作业的pod,这种数据损失随之而来。

Java SE 16(JDK16)默认启用了Z垃圾收集器(ZGC)。ZGC在根据需要释放内存方面做得更好,与应用程序并行工作,因此内存高峰得到了缓解。在JDK16下运行pod可以解决这个问题,我们的数据现在每天晚上都能成功加载。

现在我们已经看到了JDK16的强大功能,而且Java SE 17(JDK17)现在已经推出,并有一个长期的支持计划,我们决定将我们的代码库和构建系统迁移到JDK16,以利用它的许多好处。

  • 它是一个更好的JVM--并行执行更顺畅了。
  • 它是一个更好的垃圾收集器--它与主程序并行运行,在内存释放时对其进行清理。
  • 更加安全--JDK16现在默认对JDK内部进行强封装(目前可以重写,但JDK17将移除该选项,因此JDK16暂时不支持)。
  • 注意:这破坏了几个库(列在下面)。
JDK17,即下一个长期发布的JDK,已经发布。我们计划尽快迁移到JDK17。

下面是对我们在过渡期间遇到的问题的总结,以及未来升级的路径。

当时使用的框架
JDK11、Maven 3.6、Spring Boot 2.4.3、Groovy 2.5 和 3.7。

所需的框架版本
JDK16、Maven 3.8.1、Spring Boot 2.4.8、Groovy 当时不可用。

JDK 16 安装
操作系统
OpenJDK18 现在是 Homebrew 中的默认 OpenJDK。安装JDK16:
brew install openjdk@16

OpenJDK 是 Ubuntu 中的默认 JDK,可能是预安装的。
sudo apt install openjdk-16-jdk openjdk-16-source

如果安装了多个 JDK(可能是因为当前工作已经安装了 openjdk-11),请使用 update-alternatives 管理 JDK 版本:

sudo update-alternatives --config java
There are 2 choices for the alternative java (providing /usr/bin/java).
  Selection    Path                                         Priority   Status
<hr>
* 0            /usr/lib/jvm/java-16-openjdk-amd64/bin/java   1611      auto mode
  1            /usr/lib/jvm/java-11-openjdk-amd64/bin/java   1111      manual mode
  2            /usr/lib/jvm/java-16-openjdk-amd64/bin/java   1611      manual mode
Press <enter> to keep the current choice
  • , or type selection number:

    安装到用户配置文件中的目录也是运行 JDK 16 的一种很好的独立于系统的方式。下载正确的发行版,然后解压到本地目录,并将 JDK bin 目录添加到其他所有内容之前的路径(或只是 set $JAVA_HOME)。例如:

    export JAVA_HOME=~/jdk.16.0.2/
    export PATH=$JAVA_HOME/bin:$PATH


    Maven 3.8.1 Installation
    OSX
    … is easy.
    brew install maven
    or
    brew install maven@3.8

    Ubuntu 20.04
    具有讽刺意味的是,Ubuntu 20.04上的默认JDK是OpenJDK16,但maven的安装却不兼容(3.6.3)。所以我们不得不升级Maven。

    • 从Apache下载压缩包。
    • 将 tarball 解压缩到您的配置文件目录:cd ~; tar -zxvf Downloads/apache-maven-3.8.1-bin.tar.gz
    • 将 maven bin 目录添加到您的路径~/.bashrc或您运行的任何 shell:export PATH=~/apache-maven-3.8.1/bin:${PATH}
    • 打开一个新终端并仔细检查您的路径:~/Projects/r4e$ which mvn/home/user/apache-maven-3.8.1/bin/mvn
    • 构建

    Maven 更新
    Maven 3.8.6可用,包括更好的多线程构建锁定。它有点慢,但不会崩溃。


    Groovy
    Groovy 是一个特殊的挑战。使用的版本不支持 JDK17 计划的对内部的强封装,因此 Groovy 编译器只支持到 JDK15。这影响了我们的各种内部子项目。
    为了至少构建 JDK15,我们在 pom.xml 中更新了编译器插件的版本:

    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
            <compilerId>groovy-eclipse-compiler</compilerId>
            <encoding>${project.build.sourceEncoding}</encoding>
            <compilerArgument />
            <source>${java-version}</source>
            <target>${java-version}</target>
          </configuration>
          <dependencies>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-compiler</artifactId>
              <version>3.7.0</version>
            </dependency>
            <dependency>
              <groupId>org.codehaus.groovy</groupId>
              <artifactId>groovy-eclipse-batch</artifactId>
              <version>3.0.8-01</version>
            </dependency>
          </dependencies>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <configuration>
            <argLine>--illegal-access=permit</argLine>
          </configuration>
        </plugin>
      </plugins>
    </build>

    这里的关键设置是--illegal-access=permit。这使JDK16中的强封装功能失效。这就产生了以下错误:

    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project r4e-meru-scripting: Compilation failure: Compilation failure:
    [ERROR] /Users/username/git/Meru/r4e-meru-scripting/src/main/groovy/com/reputation/meru/scripting/ReusableASTTransformationCustomizer.groovy:[1,1]
    [ERROR] 1. ERROR in /Users/username/git/Meru/r4e-meru-scripting/src/main/groovy/com/reputation/meru/scripting/ReusableASTTransformationCustomizer.groovy (at line 1)
    [ERROR]  package com.reputation.meru.scripting
    [ERROR]  ^
    [ERROR] The type groovy.transform.Generated cannot be resolved. It is indirectly referenced from required .class files

    解决方法是这个hack

    Groovy 更新
    支持 JDK17的两个Maven 插件的新版本都可用。

    其他问题

    • java.security.acl 已被删除
    • 由于 Java Internals 的不可访问性,JUnit4 未能模拟任何东西。迁移到 JUnit5。
    • Apache EqualsBuilder.inspectionEquals由于 Java 内部(列表等)不可访问而失败。
    • JaCoCo 需要更新到 0.8.7。
    • PowerMock在 JDK16 中不起作用。直接使用 JUnit 5(更简单的测试)。
    • 使用 mockito-inline 模拟最终类。
    • 需要将 git commit id 插件升级到最新版本(5.0.0 和不同的父级)。
    • GSON 反序列化失败。这是在生产中发现的。修复是迁移到 Jackson 序列化器/反序列化器。谷歌的好人有一个问题,但似乎没有任何积极的工作来解决它。
    • 扩展日期格式已更改。这主要影响了我们的单元测试。没有客户抱怨日期格式的更改。
    • 法语区域设置数字分隔符现在是一个 en-space与一个简单的 ASCII 空间。也只影响了一些单元测试。
    • 可从多个模块访问的 javax.xml。该文件现在是 JDK 的一部分,因此我们必须stax-api从poi-ooxml库中排除。

    结论
    将我们的代码库从 JDK11 迁移到 JDK16 是一个很好的挑战,它使我们能够更新许多依赖项并利用 JDK 中的改进,例如内存管理和线程处理。完成向 JDK17 的迁移将有其自身的挑战,我们期待改进的安全性。