JDK升级总结

一、背景

项目升级JDK11,主要基于以下几个出发点:

  • 目前公司使用的JDK8,在2019年1月已经停止更新维护
  • 越来越多的框架和第三方库新版本不再兼容低版本JDK,如果想使用新版本的特性就必须升级。以下是一些常见的框架和第三方库,在其最新版本中需要使用JDK11及以上版本:
    • Spring Framework 5.3及以上版本
    • Hibernate ORM 5.4及以上版本
    • Apache Tomcat 10.0及以上版本
    • Jetty 10.0及以上版本
    • Apache POI 5.0及以上版本
    • Apache Lucene 8.7及以上版本
    • JUnit 5.7及以上版本
  • JDK21即将在2023年9月发布,距离JDK8已经相差了10多个版本,这些版本不断引入的新特性对于提高服务的启动、性能和内存使用情况将会有很多帮助

因此JDK升级是一个必要的过程,即使不是为了获取新特性,也应该考虑升级以获得最新的安全更新和性能优化。

二、版本选择

JDK发布周期通常是每6个月发布一个新版本(非LTS),而每个三年左右发布一个LTS(Long-term support)版本。LTS版本提供了长期的支持和维护,企业通常会优先选择LTS版本进行开发和部署。LTS版本受到官方支持和维护,因此可以得到长期的安全更新和错误修复。相比之下,非LTS版本只获得较短时间的支持,在短时间内很可能会被弃用。

目前JDK的四个LTS版本中主要以JDK8、JDK11为主,如下图所示:

对于本次升级来说,有JDK11、JDK17两个版本可供选择。

从长远来看,JDK17更代表未来,但是目前JDK11使用更加广泛,相关的升级指南和文档更加丰富,因此升级到JDK11比升级到JDK17的挑战要相对小,并且基于风险的控制,从JDK8升级到JDK11,随后再从JDK11升级到JDK17是一个更合理的选择。

因此本次升级选择JDK11,同时相应的Tomcat 7升级到Tomcat 8.5。

三、JDK11版本特性

升级前了解JDK11版本特性的必要性是非常重要的。不同版本之间会有新功能,架构变化、安全修复和性能优化等方面的不同特性。如果不了解这些新特性,就容易在应用程序开发和部署过程中遇到问题。以下是JDK11的新特性:

引入ZGC[(可伸缩,低延迟的gc)

相关JEP 333: ZGC: A Scalable Low-Latency Garbage Collector(Experimental)

JDK11引入了ZGC(实验性质,不建议用到生产环境)。ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。

ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:

  • GC 停顿时间不超过 10ms
  • 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
  • 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
  • 方便在此基础上引入新的 GC 特性和利用 colord
  • 针以及 Load barriers 优化奠定基础
  • 当前只支持 Linux/x64 位平台

HTTP Client API(正式特性)

相关JEP 323: Local-Variable Syntax for Lambda Parameters

对Http Client API 进行了标准化,并且现在完全支持异步非阻塞,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义,可以联合使用以触发相应的动作。新 Http Client API,提供了对 HTTP/2 等业界前沿标准的支持,同时也向下兼容 HTTP/1.1,精简而又友好的 API 接口,与主流开源 API(如:Apache HttpClient、Jetty、OkHttp 等)类似甚至拥有更高的性能。

    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create("http://openjdk.java.net/"))
          .build();
    client.sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::body)
          .thenAccept(System.out::println)
          .join();

Docker 容器改进

在JDK10之前,Docker容器中运行Java应用程序一直存在一个问题:容器中运行JVM程序在设置内存大小和CPU使用率后,会导致应用程序的性能下降,这是因为JVM无法识别在容器上设置的内存和CPU约束。

从JDK10开始,JVM会使用容器控制组 (cgroups) 设置的约束来设置内存和CPU限制。另外还添加了“JVM 选项”,使Docker容器用户可以精细地控制用于Java堆的系统内存量。

移除内容

这些Java EE 或 CORBA 模块中的包在JDK9弃用,在JDK11中删除。因此升级过程中,项目中使用到则需要手动引入

四、Tomcat版本区别

Tomcat 7和Tomcat 8.5之间存在以下几个区别:

  • Servlet规范:Tomcat 7支持Servlet 3.0规范,而Tomcat 8.5支持Servlet 3.1规范。
  • WebSocket支持:Tomcat 8.5对WebSocket的支持更好,Tomcat 7对WebSocket支持较弱。
  • JSP规范:Tomcat 7支持JSP 2.2规范,而Tomcat 8.5支持JSP 2.3规范。
  • 远程配置:Tomcat 8.5引入了远程配置API,允许在运行时动态修改Tomcat的配置文件,而Tomcat 7没有这个功能
  • 对多线程的支持:Tomcat 8.5对高并发的支持更好,能够处理更多的请求,而Tomcat 7在高并发时可能会出现性能瓶颈。
  • 加载jar包的顺序不同

其中,加载jar包的顺序会直接影响现有项目是否可以正常启动、运行。

Tomcat加载jar

Tomcat加载jar顺序、以及对应类加载器如下所示:

  • Bootstrap ClassLoader类加载器:加载$java_home/lib 目录下的java核心api
  • Extension ClassLoader类加载器: 加载$java_home/lib/ext 目录下的java扩展jar包
  • Application ClassLoader类加载器:加载java -classpath/-Djava.class.path所指的目录下的类与jar包
  • Common ClassLoader类加载器:$CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载
  • Catalina ClassLoader类加载器:$CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载
  • Shared ClassLoader类加载器:$CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载
  • Web App1 ClassLoader类加载器:项目路径/WEB-INF/classes下的class文件
  • Web App2 ClassLoader类加载器:项目路径/WEB-INF/lib下的jar文件

Tomcat 7通过WebappLoader.class加载WEB-INF/lib的jar包,然后在FileDirContext.class中获取文件列表后中按照文件名称首字母a-z进行排序后加载,源码如下:

Tomcat 8.5通过WebappClassLoaderBase.start()加载WEB-INF/lib的jar包,然后DirResourceSet.class中获取文件列表直接进行加载,并没有进行排序,从而导致在不同的操作系统中返回的结果不一致(Windows系统按照文件名称排序,Linux系统乱序)。源码如下:

导致的问题

由于加载jar包的顺序不同,可能会导致无法加载到原定的类。如图所示

  • 项目中存在两个相同的类LogbackServletContainerInitializer.class(packge相同,jar包不同),项目当前加载的是a-suishen-patch-libs中的
  • 在Tomcat 7中可以通过设置jar包的名称保证a-suishen-patch-libs中的先加载
  • 但是在Tomcat 8.5就无法保证加载顺序,最终导致项目无法启动

解决方式

Tomcat 8.5在StandardRoot中的list方法指定了加载资源的顺序,源码如下:

因此,我们可以通过将需要先加载的jar设置在PreResources里,确保优先加载。

设置方式:在项目的META-INF/context.xml中配置

<?xml version="1.0" encoding="UTF-8"?>  
<Context>  
    <Resources>
        <PreResources base="${catalina.base}/webapps/${project}/WEB-INF/lib/a-suishen-patch-libs-1.0-SNAPSHOT.jar"
                      className="org.apache.catalina.webresources.JarResourceSet"
                      webAppMount="/WEB-INF/classes"/>
    </Resources>
</Context>  

五、升级步骤

步骤一:代码修改

步骤二:设置JVM参数

-Xms3000m -Xmx3000m -XX:+UseCompressedOops -XX:+PrintGCDetails -Xloggc:./logs/gc.log -XX:+TieredCompilation -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heap -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:MaxTenuringThreshold=15 -XX:+AlwaysPreTouch

步骤三:项目发布

发布过程中,观察cataliana、localhost、项目日志是否有异常,项目是否启动成功。

升级注意事项:
  • cataliana、localhost、系统日志都需查看下是否有异常
  • 重要业务发布线上时一定要谨慎,必须进行全面测试

六、升级问题总结

以下是项目升级过程中遇到的问题以及对应的解决办法。

模块移除一:NoClassDefFoundError

异常如下所所示:

Caused by: java.lang.NoClassDefFoundError: javafx/util/Pair  
at java.base/java.lang.Class.getDeclaredMethods0(Native Method)  
at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3166)  
at java.base/java.lang.Class.getDeclaredMethods(Class.java:2309)  
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:613)  
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:524)  
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:510)  
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:247)  
… 31 more
Caused by: java.lang.ClassNotFoundException: javafx.util.Pair  
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1420)  
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1228)  
… 38 more

原因:JDK11移除了javafx

解决方式:手动引入依赖

<dependency>  
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>11</version>
</dependency>  
<dependency>  
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>11</version>
</dependency>  

模块移除二:NoClassDefFoundError

异常如下所所示:

[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: 
Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException  
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1080)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException  
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)

原因:Java 11 删除了 java.xml.bind (JAXB)

解决方式:手动引入依赖

<dependency>  
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
</dependency>  
<dependency>  
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.3.0</version>
</dependency>  
<dependency>  
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.3.0</version>
</dependency>  

jar包冲突 NoSuchMethodError

异常如下所所示:

org.springframework.beans.factory.BeanCreationException:  
Error creating bean with name 'authorityMenuServiceImpl': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException:  
Error creating bean with name 'roleMenuServiceImpl': Invocation of init method failed;  
nested exception is java.lang.NoSuchMethodError: org.apache.commons.beanutils.BeanUtilsBean.<init>(Lorg/apache/commons/beanutils/ConvertUtilsBean;)V  
at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:321)  
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1272)  
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)  
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)  

原因:具有相同packge的BeanUtilsBean.class在多个jar中出现,Tomcat8.5加载了项目不需要的类,导致无法找到对应的方法

解决方式:通过查看依赖树,确定具体的冲突依赖,排除不需要的jar

mvn dependency:tree  
或
./gradlew app:dependencies

javassist依赖版本不匹配

异常如下所所示:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'hessianAppService': Instantiation of bean failed; nested exception is java.lang.ExceptionInInitializerError  
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1163)
Caused by: java.lang.NullPointerException: null  
        at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:103)
        at javassist.util.proxy.DefineClassHelper.toClass3(DefineClassHelper.java:151)
        at javassist.util.proxy.DefineClassHelper.toClass2(DefineClassHelper.java:134)
        at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:95)
        at javassist.ClassPool.toClass(ClassPool.java:1143)
        at javassist.CtClass.toClass(CtClass.java:1316)
        at com.alibaba.dubbo.common.compiler.support.JavassistCompiler.doCompile(JavassistCompiler.java:123)
        at com.alibaba.dubbo.common.compiler.support.AbstractCompiler.compile(AbstractCompiler.java:59)
        at com.alibaba.dubbo.common.compiler.support.AdaptiveCompiler.compile(AdaptiveCompiler.java:46)
        at 

原因:引入的javassist的版本(3.21.0-GA)和JDK11不匹配

解决方式:javassist升级到3.23.1-GA版本

<dependency>  
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.21.0-GA</version>
</dependency>  

log4j循环依赖配

异常如下所所示:

 org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/admin]]
at org.apache.catalina.util.LifecycleBase.handleSubClassException(LifecycleBase.java:440)  
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:198)  
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:753)  
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)  
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)  
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)  
at java.base/java.lang.Thread.run(Thread.java:834)  
Caused by: java.lang.StackOverflowError  
   at org.apache.log4j.Category.<init>(Category.java:55)
   at org.apache.log4j.Logger.<init>(Logger.java:37)
   at org.apache.log4j.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:43)
   at org.apache.log4j.LogManager.getLogger(LogManager.java:45)
   at org.slf4j.impl.Log4jLoggerFactory.getLogger(Log4jLoggerFactory.java:73)

原因:log4j-over-slf4j.jar 和 slf4j-log4j12.jar、或jcl-over-slf4j.jar和slf4j-jcl.jar循环依赖导致溢出

解决方式:需要排除slf4j-log4j12、或者slf4j-jcl.jar

参考文档: https://blog.csdn.net/qq_19749625/article/details/126893801
https://www.slf4j.org/codes.html#log4jDelegationLoop

跨域问题

异常如下所所示:

01-Mar-2023 14:29:17.159 INFO [localhost-startStop-1] org.apache.catalina.core.ApplicationContext.log Initializing Spring root WebApplicationContext  
01-Mar-2023 14:29:36.839 SEVERE [localhost-startStop-1] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [CorsFilter]  
javax.servlet.ServletException: It is not allowed to configure supportsCredentials=[true] when allowedOrigins=[*]  
at org.apache.catalina.filters.CorsFilter.parseAndStore(CorsFilter.java:764)  
at org.apache.catalina.filters.CorsFilter.init(CorsFilter.java:185)  
at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:281)  
at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:262)  
at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:105)  
at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4604)  
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5255)  
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)  
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:753)  
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:727)  

原因:tomcat高版本在处理跨域问题时,当allowedOrigins=[*]时,不允许配置supportsCredentials=[true]

解决方式:配置supportsCredentials=[false]或者删除、或者考虑使用"allowedOriginPatterns"代替

七、展望

在项目升级到JDK11后,后续希望能更充分的利用JDK11、Tomcat8,5生态带来的便利性,探索启动优化、开发便捷性带来的性能提升。

作者介绍

  • 孙景亮 资深服务端开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。