一、JDK21优势
将 JDK 从 1.8(Java 8)升级到 JDK 21(LTS 版本)可以带来多方面的收益,包括性能提升、语言特性增强、开发效率提高、安全性改进等。
1.1 性能提升
- ZGC 和 Shenandoah 垃圾回收器: JDK 21 默认支持低延迟垃圾回收器,显著减少 GC 停顿时间(可控制在毫秒级),适合高吞吐量和低延迟应用。
- ZGC:支持 TB 级堆内存,停顿时间不超过 10ms。
- Shenandoah:并发回收,减少停顿时间。
- 虚拟线程(轻量级线程): JDK 21 正式引入虚拟线程(Project Loom),显著提升高并发应用的吞吐量,减少线程创建和上下文切换的开销。
- 向量化计算(Vector API): 支持 SIMD 指令(JDK 16 引入,JDK 21 进一步优化),加速数值计算和机器学习任务。
1.2 语言特性增强
-
模式匹配(Pattern Matching)
-
instanceof 模式匹配(JDK 16):简化类型检查和转换,合并类型检查和类型转换:if (obj instanceof String s) { s.length(); }
-
switch 表达式和模式匹配(JDK 17~21):支持更复杂的条件分支逻辑。
-
记录模式(Record Patterns,JDK 21):直接解构记录类(Record)的数据。
-
文本块(Text Blocks) JDK 15 引入,简化多行字符串的编写(如 JSON、SQL 等)。
1.3 开发效率提升
- 标准化 HTTP Client(JDK 11 取代 HttpURLConnection)。
- 增强的 Stream API(如 Collectors.teeing()、takeWhile()、dropWhile()、ofNullable())。
- 快速创建不可变集合:List.of("a", "b")、Map.of("k", 1)
- 文本块:多行字符串无需转义
1.4 容器化支持
-
改进的 Docker 和 Kubernetes 兼容性
-
JDK 10+ 支持容器感知的 CPU 和内存资源限制(自动识别容器环境而非宿主机)。
-
减少 JVM 在容器中的内存占用问题。
-
Native Image(GraalVM 集成) 通过 GraalVM 将 Java 应用编译为本地镜像(需额外工具支持),减少启动时间和内存占用。
1.5 长期支持(LTS)
- JDK 21 是 LTS 版本,提供长期更新支持(至少到 2031 年),适合生产环境。
版本 | 特性亮点 |
---|---|
JDK 9 | 模块化、集合工厂方法、JShell、Try-With-Resources 增强 |
JDK 10 | var局部变量类型推断 |
JDK 11 | HTTP Client 标准化、ZGC、字符串 API 增强(strip()、repeat()) |
JDK 12-15 | Switch 表达式、文本块、instanceof模式匹配(预览→正式) |
JDK 16-17 | 记录类、密封类(正式化) |
JDK 21 | 虚拟线程、结构化并发、模式匹配 Switch(正式) |
二、环境配置
2.1 下载安装JDK21
- MacOS下载
- Windows下载
2.2 升级IDEA
JDK21只有2023.2.2及以上版本IDEA才能支持,没有升级到最新版本的同学,先升级IDEA
参照 JDK21升级指南 的 "2.3 本地启动"
2.3 本地启动
File->Project Structure 检查设置




File->Settings 检查设置



启动配置
- 打开启动配置,点击Modify options

- 点击Add VM options

- 新增JVM参数
css
--add-opens=java.base/java.time=ALL-UNNAMED
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.math=ALL-UNNAMED
--add-opens=java.base/sun.reflect=ALL-UNNAMED
--add-opens=com.alibaba/spring.context=ALL-UNNAMED
--add-exports=java.base/sun.reflect.annotation=ALL-UNNAMED
--add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED

- 点击运行,起来了!

2.4 ZGC使用
在 JDK 21 中,分代 ZGC 是默认的 ZGC 模式(无需额外配置)。但如果需要显式启用或验证,可以使用以下 JVM 参数:java -XX:+UseZGC -XX:+ZGenerational ...
- -XX:+UseZGC:启用 ZGC。
- -XX:+ZGenerational:启用分代模式(JDK 21 默认开启,可省略)。
- 如果需要禁用分代模式(回退到非分代 ZGC),使用 -XX:-ZGenerational。
分代 ZGC 会自动调整年轻代和老年代比例,但可通过以下参数干预:
年轻代大小:-XX:ZYoungGenerationSize=<size 例如 -XX:ZYoungGenerationSize=2g
- 年轻代默认占堆的 10%~30%,根据应用特性调整(短生命周期对象多的应用可适当增大)。
并发 GC 线程数:-XX:ConcGCThreads=<threads 并发 GC 线程数(默认自动计算)
- 通常无需手动设置,ZGC 会根据 CPU 核心数自动分配。
最大停顿时间目标:-XX:MaxGCPauseMillis=<ms 例如 -XX:MaxGCPauseMillis=10
- ZGC 的设计目标是停顿时间不超过 10ms,此参数用于提示 GC 尽量满足要求。
大对象分配:-XX:ZAllocationSpikeTolerance=<factor 默认 2.0,控制分配速率的敏感度
- 适用于突发内存分配的场景。
NUMA 感知(多核服务器):-XX:+UseNUMA 启用 NUMA 内存分配优化
GC 日志输出:-XX:+PrintGCDetails -Xlog:gc*,gc+stats=info:file=gc.log:time,uptime:filecount=5,filesize=100m
- 输出详细的 GC 日志和统计信息,便于分析停顿时间和内存回收效率。
使用 jstat 查看内存和 GC 状态:jstat -gcutil <pid 1s 每秒输出一次 GC 统计
- 通过 JMX 工具(如 VisualVM 或 JConsole)监控堆内存和 GC 活动。
2.5 虚拟线程使用
创建虚拟线程
- 方式 1:直接使用 Thread.startVirtualThread(),这是最简单的创建方式,适用于快速测试。
Thread.startVirtualThread(() -> {System.out.println("Hello from virtual thread: " + Thread.currentThread());});
- 方式 2:使用 Thread.ofVirtual(),可以设置线程名、优先级等属性。
Thread virtualThread = Thread.ofVirtual().name("my-virtual-thread-", 1) // 线程名前缀 + 计数器.start(() -> {System.out.println("Running in virtual thread: " + Thread.currentThread());});
- 方式 3:使用 Executors.newVirtualThreadPerTaskExecutor(),推荐方式,适用于生产环境,自动管理线程池。
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {System.out.println("Task running in virtual thread: " + Thread.currentThread());});}
关键配置参数
- 虚拟线程是轻量级的,但底层仍依赖 平台线程(Carrier Threads) 来执行任务。可以调整平台线程池大小:-Djdk.virtualThreadScheduler.parallelism=256 # 默认等于 CPU 核心数。
- 如果任务有较多阻塞操作(如 I/O),可以适当增大该值。
调试和监控
- 查看虚拟线程状态:jcmd <pid Thread.dump_to_file -format=json threads.json
- 启用虚拟线程调试日志:-Djdk.traceVirtualThreads=true
tomcat线程池改成虚拟线程池
- springboot 1.x
Java @Configuration public class TomcatConfig { @Bean public TomcatServletWebServerFactory servletWebServerFactory() { TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory (); tomcatFactory.addConnectorCustomizers(connector -> { if (connector.getProtocolHandler() instanceof Http11NioProtocol) { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); protocol.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); } }); return tomcatFactory; } } - springboot 2.x -XML @Configuration public class TomcatConfig { @Bean public ServletWebServerFactory servletWebServerFactory() { TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory(); tomcatFactory.addConnectorCustomizers(connector -> { if (connector.getProtocolHandler() instanceof Http11NioProtocol) { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); protocol.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); } }); return tomcatFactory; } } - springboot3.x 支持配置修改 -Properties spring.threads.virtual.enabled=true
虚拟线程隐藏的坑
1、执行 synchronized 块/方法
-
原因: synchronized 关键字是 Java 内置的同步机制,其锁操作直接绑定到平台线程(OS 线程)。 若虚拟线程在 synchronized 块内发生阻塞(如 I/O、锁等待),JVM 无法挂起该虚拟线程,因为它需要保持平台线程对监视器锁(Monitor)的持有。
-
synchronized在jvm的实现是依赖于对象的监视器,当方法进入synchronized函数后,jvm将会记录持有对象监视器对应线程。
-
由于记录的是平台线程,所以如果在虚拟线程A进入对象obj的synchronized函数后,如果没有Pin住Carrier Thread,此时另一个虚拟线程B也被调度到了同样的Carrier Thread上执行对象obj的其他synchronized函数,此时jvm会认为虚拟线程B已经获取了对象的监视器,从而不阻塞直接进入函数内部,导致并发问题。
-
最极端的情况,如果所有的Carrier Thread都被Pin住,而synchronized外仍然有其他线程与之抢占资源,则会发生死锁。
-
解决方案:Jdk 21-23:使用JUC中的锁(比如ReentrantLock)来替换掉synchronized。包括外部依赖,比如依赖服务的SDK、使用的平台框架、中间件的SDK等,都需要使用替换掉synchronized的版本。
该问题只存在于jdk 21-23,jdk24对底层synchronized,Object.wait()以及notify()函数进行了大量的重写,经过jdk 开发团队的努力,synchronized已经不会导致Carrier Thread被Pin 了。JEP 491: Synchronize Virtual Threads without Pinning
2、调用 Native 方法或 JNI 代码
- 原因:
- Native 方法(通过 JNI 调用的 C/C++ 代码)可能直接操作平台线程的状态,或执行无法被 JVM 控制的阻塞操作。 JVM 无法感知 Native 方法中的阻塞行为,因此必须固定 Carrier Thread。
- 和上面的情况类似,如果JNI函数非常的耗时,直接阻塞导致Carrier Thread被用尽,也会发生死锁的问题。
因此使用JNI调用原生方法时,应该尽量避免耗时较长的操作。或者直接单开一个平台线程,不要挤占虚拟线程的资源。
三、依赖升级
- spring-boot系列包版本统一为2.7.18
- dubbo版本升级到2.7.12-mone-java21-v23-SNAPSHOT(虚拟线程版本)
- lombok版本1.18.30
- nacos升级到1.4.2-mone-SNAPSHOT
- miapi-doc-annos升级到mone-jdk21-SNAPSHOT
- dubbo-docs-core升级到mone-jdk21-SNAPSHOT
- commons-pool2升级到2.11.1
- maven编译插件:
- 项目父pom修改
- JavaScript org.apache.maven.plugins maven-compiler-plugin 3.11.0 21 21 21 21
- AIP模块下的pom文件修改:支持对外提供依赖包通过低版本jdk编译
- JavaScript org.apache.maven.plugins maven-compiler-plugin 3.11.0 1.8 1.8 1.8
- maven-compiler-plugin里面不能配置${project.basedir}/src/main/java,剔除掉
- JavaScript ${project.basedir}/src/main/java
- maven-surefire-plugin插件配置
- JavaScript org.apache.maven.plugins maven-surefire-plugin 3.1.2 true /backups/ /*Test.java /*Tests.java false true --add-opens=java.base/java.time=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED --add-opens=java.base/sun.reflect=ALL-UNNAMED --add-opens=com.alibaba/spring.context=ALL-UNNAMED --add-exports=java.base/sun.reflect.annotation=ALL-UNNAMED --add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED true org.junit.jupiter junit-jupiter-engine 5.8.2
四、遇到的问题
启动:必须添加VM options !!
在 Java 9 中,引入了模块化系统(Jigsaw),java.base 模块默认不会将其内部的包(如 java.util)开放给未命名模块。其中每个模块都有自己的命名空间,可以控制对其公共 API 的访问。模块可以使用 exports 关键字来导出其公共 API,其他模块可以使用 requires 关键字来声明对该模块的依赖关系。这样可以确保模块之间的隔离性和安全性。
然而,有时候我们需要在模块之间共享一些非公共的 API,这时就需要使用 --add-opens 和 --add-exports 命令行选项来解决访问限制问题。
--add-opens:命令行选项可以打开一个模块的某个包,使得其他模块可以访问该包中的非公共 API
--add-exports:命令行选项可以导出一个模块的某个包,使得其他模块可以访问该包中的公共 API
所以每一个单测和应用 Application 都要加下面的 VM Options
css
--add-opens=java.base/java.time=ALL-UNNAMED
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.util=ALL-UNNAMED
--add-opens=java.base/java.math=ALL-UNNAMED
--add-opens=java.base/sun.reflect=ALL-UNNAMED
--add-exports=java.base/sun.reflect.annotation=ALL-UNNAMED
--add-exports=java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED
--add-opens=com.alibaba/spring.context=ALL-UNNAMED
IDEA编译/启动报错:java: Cannot find JDK '21' for module 'car-service-equity-api'
解决:由于项目中不同模块用了不同的 JDK 版本,推荐勾选上 "Delegate IDE build/run actions to Maven"

img
另外,由于要求 api 模块要用 jdk1.8 编译,所以需要针对 api 模块专门配置依赖的 SDK

img

img
Nacos报错
kotlin
Failed to parse configuration class [com.xiaomi.cnzone.car.equity.server.bootstrap.CarServiceEquityBootstrap]; nested exception is java.lang.annotation.AnnotationFormatError: Invalid default: public abstract com.alibaba.nacos.api.config.ConfigType com.alibaba.nacos.spring.context.annotation.config.NacosPropertySource.type()
原因:com.alibaba.nacos.spring.context.annotation.config.NacosPropertySource注解里面的 type 属性默认值是ConfigType.UNSET(在 nacos-spring-context:1.1.2),引用了 nacos-api 包的 ConfigType 类,这个类在某些版本中没有 UNSET 这个枚举值
解决:升级 nacos-client 包版本到 1.4.2-mone-SNAPSHOT
@EnableDubbo 注解切换
解法:@EnableDubbo 注解等价于{@link DubboComponentScan} and {@link EnableDubboConfig} combination. (不切换也能启动)
Junit5单测启动报错
less
Internal Error occurred.
org.junit.platform.commons.JUnitException: TestEngine with ID 'junit-jupiter' failed to discover tests
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:160)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverSafely(EngineDiscoveryOrchestrator.java:134)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:108)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discover(EngineDiscoveryOrchestrator.java:80)
at org.junit.platform.launcher.core.DefaultLauncher.discover(DefaultLauncher.java:110)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: org.junit.platform.commons.JUnitException: ClassSelector [className = 'com.xiaomi.cnzone.car.equity.server.test.SpringReadyTest'] resolution failed
at org.junit.platform.launcher.listeners.discovery.AbortOnFailureLauncherDiscoveryListener.selectorProcessed(AbortOnFailureLauncherDiscoveryListener.java:39)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:102)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.run(EngineDiscoveryRequestResolution.java:82)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.resolve(EngineDiscoveryRequestResolver.java:113)
at org.junit.jupiter.engine.discovery.DiscoverySelectorResolver.resolveSelectors(DiscoverySelectorResolver.java:46)
at org.junit.jupiter.engine.JupiterTestEngine.discover(JupiterTestEngine.java:69)
at org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.discoverEngineRoot(EngineDiscoveryOrchestrator.java:152)
... 13 more
Caused by: org.junit.platform.commons.PreconditionViolationException: Could not load class with name: com.xiaomi.cnzone.car.equity.server.test.SpringReadyTest
at org.junit.platform.engine.discovery.ClassSelector.lambda$getJavaClass$0(ClassSelector.java:75)
at org.junit.platform.commons.function.Try$Failure.getOrThrow(Try.java:335)
at org.junit.platform.engine.discovery.ClassSelector.getJavaClass(ClassSelector.java:74)
at org.junit.jupiter.engine.discovery.ClassSelectorResolver.resolve(ClassSelectorResolver.java:66)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.lambda$resolve$2(EngineDiscoveryRequestResolution.java:134)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1685)
at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:647)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:185)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolve(EngineDiscoveryRequestResolution.java:125)
at org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolution.resolveCompletely(EngineDiscoveryRequestResolution.java:91)
... 18 more
Caused by: java.lang.ClassNotFoundException: com.xiaomi.cnzone.car.equity.server.test.SpringReadyTest
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
at org.junit.platform.commons.util.ReflectionUtils.lambda$tryToLoadClass$9(ReflectionUtils.java:829)
at org.junit.platform.commons.function.Try.lambda$call$0(Try.java:57)
at org.junit.platform.commons.function.Try.of(Try.java:93)
at org.junit.platform.commons.function.Try.call(Try.java:57)
at org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass(ReflectionUtils.java:792)
at org.junit.platform.commons.util.ReflectionUtils.tryToLoadClass(ReflectionUtils.java:748)
... 33 more
解法:maven-surefire-plugin插件必须指定 junit 的执行引擎(maven-surefire-plugin插件升级到 3.X 版本后必备)
xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
</dependency>
编译报错:Please avoid using the keyword synchronized and use the Lock mechanism for execution
解法:检查代码中是否引入了synchronized 关键字,用Lock 替换
jdk21部署到流水线后,容器里的很多linux命令都不支持
解法:jdk21和jdk8的基础镜像不一样,linux命令安装存在差异。ll 先用ls代替;vim 命令没有,在容器里面执行apt install vim安装一下
运行时异常:Unable to make field private final byte java.time.LocalTime.hour accessible: module java.base does not "opens java.time" to unnamed module
解法:需要加入以下JVM参数,使得java.time可被打开。JDK9参数 对于老项目不是太友好,只有代码跑到需要加参数得位置,才会报错。所以,上线前,得保证每一个接口,每一种场景都测到
--add-opens=java.base/java.time=ALL-UNNAMED
循环引用报错:
vbnet
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
解决方案:配置文件中声明允许循环引用-》spring.main.allow-circular-references=true
SpringBoot通过三级缓存可解决循环引用问题
Redis链接问题:Caused by: io.lettuce.core.RedisCommandExecutionException: NOAUTH Authentication required.
Lettuce 6.x版本开始,使用RESP3(Redis 6.x引入)的HELLO命令进行版本自适应判断,但是对于不支持HELLO命令的低版本实例,兼容性存在一定问题。所以对于低版本的实例,建议直接在Lettuce中指定使用RESP2协议(兼容Redis 4/5)的版本来使用。
解决方案:添加一段代码,指定RESP2协议访问Redis即可解决:
kotlin
package com.chinaroad.parking.config;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.protocol.ProtocolVersion;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
@Configuration
public class SpringConfig implements LettuceClientConfigurationBuilderCustomizer {
@Override
public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {// manually specifying RESP2
clientConfigurationBuilder.clientOptions(ClientOptions.builder()
.protocolVersion(ProtocolVersion.RESP2)
.build());
}
}
使用dubbo发起RPC调用,List类型参数报错
kotlin
Caused by: org.apache.dubbo.remoting.RemotingException: Fail to decode request due to: RpcInvocation [methodName=selectStoreByOrgIds, parameterTypes=[class com.xiaomi.cnzone.maindataapi.model.req.store.StoreListByOrgIdsRequest], arguments length=0, attachments={path=com.xiaomi.cnzone.maindataapi.api.StoreProvider, input=617, dubbo=2.0.2, version=1.0}]
解法:如果参数是List类型,不要使用List.of形式创建list,否则会RPC调用报下面报错