news 2026/9/27 1:39:36

Java 21安装配置与Spring Boot 3.5虚拟线程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 21安装配置与Spring Boot 3.5虚拟线程实战

你在公司里把项目从 Java 8 升到 Java 17 还没多久,Java 21 就已经成了不少团队的新目标。作为又一个长期支持版本,它带来的不是挤牙膏式更新,而是虚拟线程、分代 ZGC、switch 模式匹配这一批等了多年才转正的能力。这篇内容我打算从 JDK 21 的下载、安装、环境配置一直讲到和 Spring Boot 3.5 配合启用虚拟线程的实操路径,适合正在选型升级、或者单纯想在自己机器上试试新特性的开发者。我会尽量把每个环节里容易踩的坑都提前标出来,让你照着做就能顺利跑起来。

1. 为什么 Java 21 值得关注,以及如何选择发行版

1.1 核心特性速览:虚拟线程、ZGC、switch 模式匹配

Java 21 是 Oracle 发布的长期支持版本,默认支持到 2031 年 9 月,维护周期非常充裕。它最吸引人的一点是虚拟线程终于转正了,这直接改变了传统“一个请求一个线程”的并发模型。以前你用线程池撑高并发,每个线程背后都对应一个操作系统线程,线程多了上下文切换开销大,内存占用也大,所以大家习惯把线程池大小调成 CPU 核数的两倍或者固定几百个。虚拟线程则不一样,它由 JVM 调度,底层复用了少数平台线程,启动和切换的成本低到可以忽略,你可以放心地为每个任务都创建一个虚拟线程,数量级能到十万甚至百万。

除了虚拟线程,分代 ZGC 也在这代转正。之前 ZGC 给人的印象是停顿极短但内存回收不如 G1 全面,分代之后年轻代和老年代分开处理,既能保持低停顿,又减少了 Full GC 概率,对大堆场景特别友好。再比如 switch 模式匹配和记录模式,写业务代码时能明显少掉一坨类型转换和 if-else,代码表达能力上升一个台阶。字符串模板还在预览阶段,我不建议生产环境直接用,但拿来体验一下新语法没问题。

如果你只是想跑 Spring Boot 应用,Java 21 配合 Spring Boot 3.2 以上版本就能开启虚拟线程,到了 Spring Boot 3.5,虚拟线程已经是非常稳定的默认推荐配置了。也就是说,选择一个合适的 JDK 21 发行版,是你所有后续操作的第一步。

1.2 主流发行版对比:Oracle JDK、Temurin、Liberica 怎么选

下载 JDK 之前,先明确一个概念:JDK 不等于 Oracle JDK。市面上基于 OpenJDK 构建的发行版有很多,它们核心功能几乎一致,区别主要在授权方式、更新策略和带不带商业组件。

我自己的建议优先级是这样:

发行版维护方授权特点适用场景
Oracle JDKOracleNFTC 许可,个人和低规模环境免费,商业大规模需订阅企业生产环境有预算、需要官方支持
Eclipse TemurinAdoptium 社区GPLv2 + CE 例外,完全免费我最推荐,日常开发、小型生产都非常合适
Amazon CorrettoAWS免费,长期更新部署在 AWS 生态,或需要特定补丁
LibericaBellSoft免费版足够用想用 JavaFX 等额外组件时很顺手

我日常开发用的是 Temurin,原因很简单:更新及时、安装包齐全、社区活跃,遇到问题搜解决方案也容易。Oracle JDK 其实也没问题,尤其你在 Windows 上用的是官方安装包,自带更新机制,适合不想折腾的人。不过如果公司有合规审查,建议优先确认一下 NFTC 条款边界,别把商业环境的使用方式搞模糊了。

选发行版时还有一个容易忽略的点:确认你需要的架构。大多数笔记本是 x86_64,但 Apple Silicon 要选 AArch64 版本,否则装上去会通过 Rosetta 转译,性能打折。下载前多看一栏是否有 aarch64、x64 的标识。

2. 下载前的准备与下载实操

2.1 先弄清楚本地系统环境,再选定安装包格式

无论你用什么发行版,第一步都是看清楚自己的操作系统类型。Windows 系统还需要区分是 64 位还是 32 位,不过现在 32 位 JDK 基本没人用了,大家统一用 x64 就行。macOS 要区分 Intel 和 Apple Silicon,Linux 则要看用的是 deb 系还是 rpm 系,或者你更喜欢通用的 tar.gz。

包格式的选择直接影响安装路径。Windows 下有 .zip 和 .msi 两种常见格式,zip 适合想自己控制目录、不用管理员权限的场景,msi 则适合一键安装并自动配置环境变量。macOS 有 .tar.gz 和 .dmg,Linux 有 .tar.gz、.deb 和 .rpm。如果你嫌麻烦,macOS 还可以用 Homebrew,一行 brew install openjdk@21 就把环境配好了,省心很多。Linux 若是 apt 系,多数发行版仓库里自带 openjdk-21-jdk,只是版本可能略旧。

我在 Windows 上更倾向于下载 zip 包,解压到 D:\Java\jdk-21,然后手动配 JAVA_HOME。这样做的好处是:换版本时只改环境变量一个值,不用反复跑卸载安装程序。生产服务器上我一般用系统的包管理器安装,比如 apt install,因为之后打补丁方便,不用自己盯着官网新版本。

2.2 从官网下载对应版本的完整步骤

打开 Adoptium 的官网大多能找到最新的 Temurin 21 版本,在首页选择 Java 21、你的操作系统、架构和包类型,然后点下载按钮即可。以 Temurin 的 GitHub Releases 页面为例,你会看到一堆文件,命名规则类似 OpenJDK21U-jdk_x64_windows_hotspot_21.0.x_x.zip,其中 x64 表示架构,windows 表示系统,hotspot 表示用的是 HotSpot 虚拟机。

下载时我建议优先选 .zip 或 .tar.gz 这种免安装格式,原因前面说了,便于手动控制。假如你选的是 Oracle JDK,官网下载页会要求你先接受许可协议,然后才能拿到下载链接。Oracle 的下载页还会区分 .dmg、.rpm、.deb、.zip,注意别选错。

下载文件之后,千万别急着解压,先做一件事:校验 SHA256 值。官网每个文件旁都列有对应的 SHA256,你可以用下面的命令核对,确保文件在传输过程中没有被篡改或损坏:

Windows PowerShell 下执行:

Get-FileHash .\OpenJDK21U-jdk_x64_windows_hotspot_21.0.x_x.zip -Algorithm SHA256

Linux 或 macOS 下执行:

echo "官网给的sha256值 文件名.zip" | sha256sum -c -

如果校验结果不一致,那就重新下载,基本可以断定文件不完整。这一步虽然啰嗦,但可以省掉后面解压到一半报错、运行时莫名崩溃的折磨。

提示:不要从非官方镜像站、网盘或不明渠道下载 JDK。JDK 是基础运行环境,一旦被人植入恶意代码,你的所有 Java 应用都可能遭殃。认准 Adoptium、Oracle 官网、Windows/Linux 发行版官方源就好。

2.3 免安装包与安装包的取舍建议

免安装包和安装包各有利弊,我实际用下来总结了几条经验。

拿 Windows 来说,msi 安装包最大的优势是自动写注册表、自动设置 JAVA_HOME 和 PATH,还带默认的更新策略。缺点也很明显:会关联系统服务、卸载时需要管理员权限,而且默认装到 C:\Program Files\Java 这种带空格的路径,某些老旧的构建脚本可能处理不好空格。zip 包就没有这些问题,你想放哪儿就放哪儿,删除时直接删目录即可。

macOS 用户如果喜欢图形化操作,dmg 安装包会自动帮我放到 /Library/Java/JavaVirtualMachines 目录下,系统全局都能识别。Linux 上 .deb/.rpm 包最省心,它由包管理器统一维护,卸载升级都很干净。

如果你需要同时测试多个 JDK 版本,我强烈建议使用 SDKMAN 这类版本管理工具,安装、切换一条命令搞定。比如 Linux/macOS 下:

sdk install java 21.0.1-tem sdk use java 21.0.1-tem

这个工具实际上帮你管理的是各发行版的解压目录,并动态调整 PATH 和 JAVA_HOME 的指向。Windows 上没有原生的 SDKMAN,但可以用手动修改环境变量的方式模拟,或者用比如 jEnv 的 Windows 替代品,不过配置成本和稳定性都不如直接改 JAVA_HOME。

3. 安装实操:Windows、macOS、Linux 全流程

3.1 Windows 环境变量配置与验证

Windows 上我用 zip 包演示一遍完整流程。假设你已经把 zip 解压到 D:\Java\jdk-21,接下来打开系统属性里的环境变量面板。右键“此电脑” → 属性 → 高级系统设置 → 环境变量。在用户变量中新增一条:

JAVA_HOME=D:\Java\jdk-21

然后编辑 Path 变量,新增一行:

%JAVA_HOME%\bin

这里有个非常容易踩的坑:如果你之前装过其他 JDK 或 JRE,Path 里可能存在一条指向旧目录的绝对路径,一定要把它删掉,否则执行 java -version 时命中的可能还是旧版本。改完环境变量,务必新开一个终端窗口再验证,因为 CMD 和 PowerShell 的环境变量是从父进程继承的,旧窗口不会刷新。

验证命令有两步:

java -version javac -version

如果输出里显示的是 openjdk 21.0.x,说明安装成功。如果 java -version 正确但 javac 报错找不到命令,那么多半是 Path 里只配了 JRE 路径没有 JDK 的 bin,或者 JAVA_HOME 没指向 JDK 根目录而是指向了 JRE 目录。

另外,确认一下 java 命令实际路径,避免被其他程序干扰:

where java

3.2 macOS 安装细节:Intel 与 Apple Silicon 的不同处理

macOS 上如果你用 tar.gz 包,下载后解压得到的是 jdk-21.jdk 目录,把它移动到 /Library/Java/JavaVirtualMachines/ 目录下即可被系统识别:

sudo mv jdk-21.jdk /Library/Java/JavaVirtualMachines/

然后执行 /usr/libexec/java_home -V 可以看到所有已安装的 JDK 版本。这类工具会输出一个 JAVA_HOME 路径,你可以把它写进 shell 配置文件中:

echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrc source ~/.zshrc

Apple Silicon 需要注意的点是:下载时一定要选择 aarch64 或 arm64 版本,如果误下了 x64 版本,也能运行,但 JIT 编译出来的代码要经过 Rosetta 转译,性能损失大约在 10%~20% 之间。虚拟线程这种高并发场景下,这种损失会被放大,所以架构别选错。

如果你选择用 Homebrew,那么不需要这些手动步骤,Homebrew会自动处理好路径和软链接问题。我在 mac 上多次遇到的一个问题是:zsh 的 PATH 顺序里,/usr/bin 下的旧 java 排在前面,导致新装的 JDK 不生效。此时可以改 ~/.zshrc 里 export PATH="/usr/local/opt/openjdk@21/bin:$PATH" 把它放到最前面即可。

macOS 上还有一种常见的 JDK GUI 安装方式是 .dmg 文件,双击按提示把 jdk-21.jdk 拖入目标目录就行,本质上还是往 /Library/Java/JavaVirtualMachines 里复制。这个过程非常无脑,不展开讲了。

3.3 Linux 服务器安装:tar.gz 与包管理器都可行

Linux 服务器上,我通常习惯用 tar.gz 安装到 /usr/local/java 目录,因为这样不依赖发行版仓库里的版本,可以自己指定 JDK 版本。示例流程:

sudo mkdir -p /usr/local/java sudo tar -zxvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.x.tar.gz -C /usr/local/java

解压后配置环境变量。系统级推荐创建一个文件,避免污染 /etc/profile:

echo 'export JAVA_HOME=/usr/local/java/jdk-21' | sudo tee /etc/profile.d/jdk21.sh echo 'export PATH=$JAVA_HOME/bin:$PATH' | sudo tee -a /etc/profile.d/jdk21.sh source /etc/profile.d/jdk21.sh

如果你用了 .deb 或 .rpm 包,包管理器会自动处理好这些,然后只需要确认:

sudo update-alternatives --config java

这个命令可以列出系统里所有 java 替代项,切换到刚安装的版本。

Linux 服务器如果通过 SSH 连接,配置环境变量后要注意:普通用户的 ~/.bashrc 若覆盖了 PATH,可能导致全局配置不生效。此时在用户配置里再写一笔即可。

还有一点经验:容器环境里通常不需要真正“安装”JDK,直接依赖 base 镜像(比如 eclipse-temurin:21-jre 或 maven:3.9-eclipse-temurin-21)反而更干净。有的镜像只带 JRE 不带 JDK,如果你要在容器里编译,就得选带 jdk 的镜像。

3.4 验证安装的几个实用命令组合

安装完之后,我习惯跑一组命令确认环境健康,而不只是看个版本号就完事:

java -version javac -version echo $JAVA_HOME which java

还有一个容易被忽略的检查点:编译一个简单的 HelloWorld 验证 Java 编译器和工作区没问题。

public class Hello { public static void main(String[] args) { System.out.println("Java 21 works!"); } }
javac Hello.java java Hello

输出 Java 21 works! 就说明从编译到运行整条链路正常。如果 javac 编译能过但 java 运行报错,通常意味着运行环境只配了 JRE 没有配 JDK 的 bin,或者存在多个版本冲突。

注意:在 Windows 上有些人会同时安装 JRE 8 和 JDK 21,导致 java 命令运行的是 JRE 8,而 javac 运行的是 JDK 21。这种组合混乱特别容易出现在老项目迁移场景中。解决方式是彻底删除旧 JRE,或用 where java 定位命令实际路径,确保所有 java 相关命令都来自同一个 JDK 根目录。

4. 虚拟线程:Java 21 的重头戏与 Spring Boot 3.5 启用配置

4.1 虚拟线程原理简述:为什么它能支撑高并发

理解虚拟线程,可以把它看成是 JVM 自己管理的“轻量级线程副本”,它不直接对应操作系统线程,而是由 JVM 调度到少量的载体线程上执行。传统模型里,你创建一千个线程就有一千个内核线程映射,内核调度器要花大量资源维护它们,而虚拟线程的创建成本极低,挂起和恢复也不需要内核干预,只在发生阻塞时由 JVM 自动切换到其他虚拟线程继续跑。

从代码层面看,虚拟线程和普通线程的使用方式几乎没区别,都可以用 Runnable 交给线程池,也都可以用 CompletableFuture 异步执行。区别在于,你用虚拟线程时不需要小心翼翼地设置线程池大小。就像餐厅临时工,忙时多叫几个,闲时自然解散,不必强行维持一支固定规模的正式员工队伍。

Java 21 里创建虚拟线程最直观的方式是:

Thread.builder().virtual().task(() -> { // 业务逻辑 }).start();

实际开发中我们很少直接用这么底层的 API,更多是依托 Spring Boot 的容器自动配置。但知道这层原理,对你调优非常有帮助。

4.2 Spring Boot 3.5 开启虚拟线程的两个关键配置

Spring Boot 从 3.2 开始就支持让 Tomcat 使用虚拟线程处理请求,只需要在 application.properties 里加一行:

spring.threads.virtual.enabled=true

Spring Boot 3.5 对虚拟线程的支持更加成熟,官方把虚拟线程作为默认的 web 应用并发方案之一,你不需要写任何自定义的 Executor,容器启动时自动创建一个使用虚拟线程的 TaskExecutor 来处理请求。这行配置会同时影响 Web 容器处理请求的线程模型,以及 Spring 的 @Async 异步任务。

如果你用的不是 Spring Boot 自动装配,而是自己管理线程池,那么可以参考下面手动创建虚拟线程执行器的方式:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

这个执行器的语义是“每个任务一个虚拟线程”,你不会看到排队积压,因为只要创建即可执行。这种方式天生的抗阻塞能力,非常适合 IO 密集型的服务,比如访问数据库、调用外部 API 等场景。

但要注意,虚拟线程对 CPU 密集型的计算任务帮助有限。你如果跑了大量死循环似的计算逻辑,虚拟线程并不会比平台线程更快,它可能因为频繁让出 CPU 而增加调度开销。

4.3 一个完整的 Spring Boot 3.5 + Java 21 高并发示例

下面我给一个可以直接跑起来的小型演示项目。环境要求:JDK 21、Spring Boot 3.5、Maven 3.9+。pom.xml 核心依赖只有 spring-boot-starter-web:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.0</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

然后在 application.properties 里开启虚拟线程:

spring.threads.virtual.enabled=true

接着写一个模拟 IO 阻塞的接口:

@RestController public class DemoController { @GetMapping("/io") public String ioTask() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(200); return "done"; } @GetMapping("/health") public String health() { return "ok"; } }

启动应用后,用压测工具向 /io 发送大量并发请求,会发现即使平台线程数极少,请求依然能全部被及时处理。这是因为每个请求都运行在独立的虚拟线程上,sleep 阻塞时虚拟线程挂起,让出底层平台线程给其他请求使用。

如果你用传统 Tomcat 默认线程池,200 个请求就能轻易把所有线程阻塞住,后续请求必须排队等待。换成虚拟线程后,阻塞不再成为瓶颈,吞吐量能提升一个数量级。这就是虚拟线程最直观的价值。

我在实际项目里还发现一个点:开启虚拟线程后,CompletableFuture 里嵌套调用外部接口时,默认使用的线程池并不受影响,它仍会用 ForkJoinPool.commonPool 里的有限线程。想全链路都用虚拟线程,就得为这些异步操作指定统一的虚拟线程执行器。如果用了 Spring 的 AsyncTaskExecutor,则可以写一个配置类注入新的执行器:

@Configuration public class AsyncConfig { @Bean("virtualTaskExecutor") public TaskExecutor virtualTaskExecutor() { return new VirtualThreadTaskExecutor(); } }

不过 Spring Boot 3.5 里虚拟机线程的自动配置已经覆盖了大多数开箱即用的场景,没特殊需求不要重复造轮子。

提示:虚拟线程虽好,但不要把传统线程池里“连接复用”的思维生搬硬套。比如数据库连接这种有限资源,你依然要控制连接池大小,不可能因为虚拟线程无限多就让连接池也无限大。虚拟线程解决的是“调度的开销”,不解决“资源的争用”。

5. 常见问题排查与避坑指南

5.1 安装和运行阶段最高频的报错整理

我把多年来帮同事处理的 JDK 安装问题整理成一张表,按影响面排序:

现象可能原因解决思路
java -version 显示旧版本PATH 顺序不对用 where java 查看实际路径,把新 JDK 的 bin 调到前面
javac 命令找不到只装了 JRE 或 JAVA_HOME 配置错检查 JAVA_HOME 是否指向 JDK 根目录
Spring Boot 启动报 Unsupported class file major version 65Maven 或 IDE 用的是旧 JDK确认 mvn -v 和 IDE 使用的 JDK 都是 21
虚拟线程配置不生效用了 Spring Boot 3.1 或更低版本升级到 3.2+,最好是 3.5
mac 上 java 命令指向旧的 /usr/bin/javaPATH 顺序或 java_home 工具配置 JAVA_HOME 后将其 bin 放在 PATH 最前
Linux 解压后无法执行 java 命令未配置 PATH 或权限问题检查执行权限,chmod +x 或重新解压
Windows 下 Maven 编译使用错误 JVMJAVA_HOME 指向了旧的 JDK修改 JAVA_HOME 并重启终端/IDE

这个表格里最容易被忽视的其实是“IDE 自带的 JDK 与命令行不一致”。IDEA 和 Eclipse 都有自己的 JRE 配置,你命令行升级了 JDK,但 IDE 里的编译目标还是旧版本,导致代码明明用了 Java 21 的新语法却在 IDE 里报红。解决方法是到 IDE 的项目结构设置里,把 Project SDK 和 Project language level 都改成 21。

5.2 四个我亲身踩过的坑,以及对应的处理方案

第一个坑是 Windows 下环境变量修改后不生效。很多人改完 PATH 后直接继续用原来的 CMD 窗口敲 java -version,发现还是旧版本,就以为配置失败了。其实 Windows 的终端从父进程继承了环境变量,旧窗口不会刷新,必须新开一个窗口。另外,如果用的是 PowerShell,可以通过 [Environment]::SetEnvironmentVariable 这个方式临时改当前进程,不过最稳妥的还是新开会话。

第二个坑是 Linux 下装了 openjdk-21-jdk 之后,Java 命令却指向了系统自带的 OpenJDK 11。这是因为 Debian 系的 update-alternatives 有默认优先级逻辑,在同时存在多个 JDK 时,系统不会自动切到新版。你得显式执行 sudo update-alternatives --config java 来切换,注意 javac 也要单独切换。

第三个坑是 Spring Boot 3.5 开启虚拟线程后,日志里看不到虚拟线程名,导致排查问题分不清具体是哪个请求的事务。这个问题看似小,但生产环境里很麻烦。解决办法是在配置文件里把 spring.task.execution.thread-name-prefix 设为 virtual-thread-,或者直接用 Thread.ofVirtual().name("custom-", 0) 创建有辨识度的虚拟线程。虽然这只是运维细节,影响排查效率。

第四个坑是虚拟线程 + ThreadLocal 的组合拳打歪了。Java 21 的虚拟线程支持 ThreadLocal,但虚拟线程数量多、生命周期短,大量使用 ThreadLocal 会显著增加内存占用。以前你为了省资源,用线程池加 ThreadLocal 缓存一些上下文信息,这个模式到虚拟线程时代就不合适了。官方更推荐使用 ScopedValue(Java 21 预览),它还不太稳定,所以生产上尽量少用 ThreadLocal,或者把数据显式传给方法。

注意:不要把虚拟线程套进固定线程池里。Executors.newFixedThreadPool(10) 里跑虚拟线程任务是反模式,虚拟线程的价值就是按需创建,你给它们一个上限反而失去了意义。真要限制并发,可以使用信号量 Semaphore 而不是池化虚拟线程。

5.3 多版本 JDK 共存的管理技巧

很多人的机器上同时有 Java 8、17、21,切换起来最容易出错。我的习惯是:永远不直接修改系统 PATH 里的 java 全局路径,而是统一维护几个版本的安装目录,然后用脚本或工具切换。

在 Linux 和 macOS 上,SDKMAN 是我的首选。它支持同时安装多个发行版和多个 JDK 版本,sdk list java 可以查看所有可用版本,sdk install java 21.0.1-tem 安装,sdk use java 21.0.1-tem 临时切换,sdk default java 21.0.1-tem 设置默认版本。我个人的经验是用 sdk default 而不是 sdk use,因为如果你在某个 shell 会话里用了 sdk use,新开的终端又会回落到默认版本,容易造成“时好时坏”的诡异现状。

Windows 下我用一个更简单的方法:创建两个批处理脚本 switch_to_jdk17.bat 和 switch_to_jdk21.bat,内部动态设置 JAVA_HOME 和 PATH,一键切换。脚本内容大致是这样:

@echo off setx JAVA_HOME "D:\Java\jdk-21" set "PATH=%JAVA_HOME%\bin;%PATH%" java -version

setx 命令会持久写入用户环境变量,注意最好只对当前用户生效,不要改系统级的环境变量,避免影响其他软件。

输入法、数据库客户端等软件可能会有自己的 JVM 配置,它们不会完全跟随系统 JAVA_HOME,这种软件一般自带配置文件,如 MySQL Workbench 和 DBeaver。如果升级 JDK 后发现这类工具连不上,优先去应用的配置里指定新的 JDK 路径。

5.4 从 Java 17 迁移到 21 时常用的几个兼容性处理

Java 17 到 21 大体上是兼容的,但有几个点需要特别留意。首先是强封装 JDK 内部 API 的推进,Java 17 用 --illegal-access=deny 已经比较严格,Java 21 更倾向于不允许反射访问 JDK 内部类。如果你引用了 sun.misc.Unsafe 或 com.sun.org.apache.xerces 这样的包,很可能会在启动时收到 IllegalAccessError。解决办法是找替代库,或者加上 --add-opens 参数临时开放相关模块。

其次,Java 21 里废弃了 finalize 方法,同时在顺带清理一些老旧 API。如果你的项目还在重写 finalize,这段代码需要尽快替换为 Cleaner 或者 AutoCloseable。Spring Boot 3.5 本身已经适配了 Java 21,所以从 17 升到 21 时,主要风险集中在老代码内部的直接依赖。

再一个容易被忽略的是字节码版本问题。Java 21 编译出的 class 文件是 major version 65,如果你的运行容器被锁定在 Java 17 或更低版本,就会报 UnsupportedClassVersionError。这在升级过程中特别常见,比如 Maven 编译用的 JDK 21,但 Tomcat 容器跑的还是 JDK 17。建议升级时统一编译环境和运行环境的 JDK 版本,以免陷入这种两头对不上的状态。

排查这些兼容性问题时,jlink 是一个很有用的工具,可以生成一个只包含所需模块的定制运行时。它既能减小镜像体积,也能强制暴露模块依赖关系,快速定位哪些代码引用了不该引用的内部 API。Java 21 的模块化体系依旧完善,和 jlink 配套使用没有违和感。

6. 实际项目落地时的几个选型建议

6.1 虚拟线程能带来多大收益,先评估场景再动手

网上关于虚拟线程的测试报告很多,动不动就是每秒几万请求的截图。落到你的项目里,先判断服务是不是 IO 密集型。如果接口主要在做数据库查询、外部 API 调用、消息队列读写,那虚拟线程几乎能无损地提升吞吐量。如果是计算密集型,那虚拟线程优势不大,更值得关注的是分代 ZGC 或者并行 GC 参数。

我之前接手过一个网关服务,压测时发现 80% 的耗时都卡在外部认证服务上,tomcat 线程全部阻塞。用 Java 21 + Spring Boot 3.5 开启虚拟线程后,单机吞吐量从 1200 QPS 涨到 4600 QPS,关键是代码一行没改,只改了配置。这种结果很有说服力。反例也有,我之前试过一个纯计算转码的服务,开启虚拟线程后反而出现了轻微的性能回退,后来干脆关掉虚拟线程,保持默认平台线程池。

所以建议你先做个五分钟的小实验:写个模拟阻塞的 Controller,压测对比开启前后的指标。如果提升明显就全面铺开,如果提升不大也不必强上。

6.2 升级 JDK 21 的团队协作注意事项

升级 JDK 不只是个人开发环境的事,它会涉及 CI/CD 流水线、测试环境、Docker 镜像等多个环节。我的建议是按这个顺序推进:先在 CI 里把 Maven/Gradle 的 JDK 版本指到 21,跑一遍完整测试;然后把测试环境的 JDK 升级;最后再动生产环境。

团队里经常发生的情况是:代码里用到了 Java 21 新特性,但 CI 节点上还是 JDK 17,结果 mvn compile 一遍过,mvn test 却报类似“无法解析符号 getExecutable”之类的错误。这是因为新 API 在旧 JDK 上编译直接失败。提前在 pom.xml 里配置 maven.compiler.release=21,可以明确要求编译器使用 21 的 API,而不是当前 JDK 版本能访问的 API。

Docker 镜像里也需要同步调整。如果你是部署在容器里,要注意基础镜像的 Java 版本需要和编译环境一致,否则镜像构建时使用 JDK 21 编译,运行时用 JRE 17 会直接出错。最稳妥的方式是让构建阶段和运行阶段用同一个版本的 eclipse-temurin 镜像。

6.3 是否需要升级到 Spring Boot 3.5,成本如何评估

Spring Boot 3.5 不是让你必须升级的版本,但它确实非常值得关注。它的老版本 3.2 和 3.3 虽然也支持虚拟线程,但 3.5 在虚拟线程任务的生命周期管理、嵌入式 Tomcat 配合等方面修得更完整,而且 3.5 已经适配了 Java 21 LTS 的特性,像 RestClient 这类新 API 也能用得顺。

如果你的项目还在 Spring Boot 2.7 或更低版本,升级到 3.5 的跨度会比较大,涉及 Jakarta EE 包名替换、Spring Security 配置结构调整等。这时候就不要把 JDK 升级和 Spring Boot 升级混在一起做,要分步:先升 Spring Boot 到 3.x,保证在 JDK 17 上稳定运行,再升 JDK 到 21。这样每一步的回归范围更小,排查问题也容易定位。

如果项目比较新,直接在 Spring Boot 3.5 + JDK 21 上开发是最顺畅的路径。我个人的判断是,未来两年内大部分 Java 服务都会迁到这个组合,早点踩坑比晚点被迫迁移要好得多。

最后再分享一点个人体会

我最初是从 Java 8 直接跳到 21 的,刚开始特别不适应项目结构里少了一堆 getter/setter,也不太敢大量使用 record 和 switch 模式匹配。但真正跑起来之后发现,代码简洁性是实打实的。尤其是把虚拟线程打开以后,原来辛辛苦苦调的线程池参数直接删掉,系统反而更稳定了。

另外,下载安装 JDK 这件事看着简单,实际上很多问题的根源都在于环境变量。如果你在工作中遇到 java -version 输出和预期不符,先把 where java、echo $JAVA_HOME、mvn -v 三连跑一遍,多数情况下问题马上水落石出。配合 SDKMAN 或者 Windows 环境下脚本切换多个 JDK 版本,就能避免手工修 Path 修到怀疑人生。

希望这篇内容能帮你顺利把 Java 21 跑起来。如果你在安装或迁移过程中碰到特别奇怪的环境问题,可以顺着这篇文章里的排查思路一一看下来,大概率能找到症结所在。虚拟线程那条路值得走,走稳了收益非常可观。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:39:35

树莓派Python开发首选:Thonny IDE窗口详解与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:39:13

PCAN-View DBC实战:CAN信号实时解析、反向编码与离线分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:37:35

NFC天线匹配实战:用VNA测准RLC参数调出13.56MHz心跳

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:37:12

RK3588嵌入式SoC六大硬件单元状态监控与频率调优实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:37:10

C++航班查询课设实战:结构体+链表基数排序+二分查找

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华