
JDK 21 是继 JDK 17 之后的又一个 LTS 版本虚拟线程转正、分代 ZGC 落地、Record Patterns 和 Pattern Matching for switch 正式定型这几个特性凑在一起让它成了近几年最值得升级的一个版本。但问题也随之而来很多人兴冲冲装完 JDK 21打开 IDEA 却发现语言级别最高只到 17或者项目能编译但一跑就报Unsupported class file major version 65再或者虚拟线程的 API 死活提示找不到符号。这些坑我在最近几个项目里几乎挨个踩了一遍所以这篇文章就把 IntelliJ IDEA 2023.2 配置 JDK 21 的完整流程、每个环节背后的逻辑、以及实战新特性时容易翻车的地方一次性讲透。不管你是刚接触 IDEA 的新手还是用了好几年但一直没搞明白 SDK、语言级别、字节码版本这三者关系的老手看完都能直接照着操作把 JDK 21 跑起来。1. 升级前的整体思路与版本选型1.1 为什么偏偏是 IDEA 2023.2 配 JDK 21先说版本搭配这件事因为它决定了你后面会不会白折腾。JDK 21 正式发布于 2023 年 9 月而 IDEA 2023.2 是同年 7 月底的版本严格来说它发布时 JDK 21 还没 GA。那为什么还推荐这个组合因为 IDEA 2023.2 已经内置了对 JDK 21 的预览支持尤其是对虚拟线程调试、结构化并发这些特性的识别做得比较到位后续的 2023.2.x 小版本又补了不少兼容性修复。如果你用的是 2023.1 或更早配置 JDK 21 时语言级别下拉框里可能压根找不到 21 这个选项只能手动填体验很差。这里有个关键认知要先建立起来IDEA 版本、JDK 版本、项目语言级别、字节码版本这是四个独立的东西。很多人把它们混为一谈导致配置时顾此失彼。IDEA 版本决定它“认不认识”某个 JDKJDK 版本是你实际安装的运行时语言级别决定编译器允许你用哪些语法字节码版本决定编译出来的 class 文件能被哪个版本的 JVM 加载。后面所有的坑本质上都是这四个东西没对齐。1.2 JDK 21 到底带来了哪些值得升级的东西如果只是版本号好看那没必要折腾。JDK 21 真正吸引人的是几个转正的重磅特性。虚拟线程Virtual Threads从预览转正这是 Project Loom 的核心成果用极轻量的线程模型把高并发 IO 场景的吞吐量拉高一个量级而且写法几乎和普通线程一样。分代 ZGC转正把 ZGC 的停顿时间优势扩展到了分代场景对大堆内存应用很友好。语言层面Record Patterns和Pattern Matching for switch正式定型模式匹配终于可以放心用在生产代码里。还有Sequenced Collections这个新集合接口给有序集合补上了统一的头尾操作 API。这些特性里虚拟线程是绝大多数人升级 JDK 21 的直接动机也是配置过程中最容易出问题的地方——因为它涉及 API 可用性和运行时行为的双重变化光把语言级别调到 21 还不够。1.3 升级前必须确认的三件事动手之前先花五分钟确认三件事能省掉后面大量返工。第一确认你现有的项目依赖有没有和 JDK 21 冲突的特别是一些老版本的字节码增强库比如某些版本的 ASM、CGLIB它们可能不认识 major version 65 的 class 文件。第二确认构建工具版本Maven 需要 3.9.x 以上、Gradle 需要 8.4 以上才对 JDK 21 有完整支持版本太低会在编译阶段就报错。第三确认团队协作环境如果项目里有人还在用 JDK 8 或 11那你本地升到 21 编译出来的产物可能别人跑不了这种情况要么统一升级要么用工具链Toolchain隔离。提示升级 JDK 大版本前强烈建议先在独立分支上验证不要直接在主开发分支上动。JDK 21 虽然兼容性做得不错但字节码版本提升后任何依赖字节码操作的库都可能出问题。2. JDK 21 的下载安装与环境变量配置2.1 选哪个发行版别只盯着一个来源JDK 21 有很多发行版常见的有 Oracle JDK、Eclipse TemurinAdoptium、Amazon Corretto、Azul Zulu、Microsoft Build of OpenJDK 等。它们都基于同一套 OpenJDK 源码核心功能一致区别主要在许可证、长期维护承诺和附带的工具上。对于个人开发和大多数企业项目Eclipse Temurin是比较稳妥的选择社区活跃、更新及时、没有许可证顾虑。如果你在公司环境里可能已经有统一的发行版规范那就跟着规范走。选发行版时要注意一点不同发行版对虚拟线程的默认调度参数可能略有差异虽然 API 层面完全一致但在压测时表现会有细微不同。这个差异不影响开发但如果你要做性能基准测试最好固定用一个发行版别混着测。2.2 安装路径的选择与命名习惯安装路径这件事看似小事其实影响很大。我见过太多人把 JDK 装在带空格或中文的路径下比如C:\Program Files\Java\jdk-21或者D:\开发工具\jdk21然后在配置 IDEA 或构建工具时各种报路径错误。强烈建议用纯英文、无空格、层级浅的路径比如D:\dev\jdk\jdk-21或/opt/jdk/jdk-21。另外如果你机器上同时有多个 JDK 版本这几乎是必然的建议在路径命名上带上版本号比如jdk-17、jdk-21而不是笼统的jdk。这样后面配置环境变量和 IDEA SDK 时一目了然不会搞混。我自己习惯在D:\dev\jdk\下按版本建目录切换时改环境变量或者直接在 IDEA 里指定路径就行。2.3 环境变量的配置逻辑环境变量配置是新手最容易出错的地方核心是要理解JAVA_HOME和PATH的分工。JAVA_HOME指向 JDK 的根目录很多工具包括 IDEA、Maven、Gradle都靠它来定位 JDKPATH里则要加入%JAVA_HOME%\binWindows或$JAVA_HOME/binLinux/macOS这样命令行才能直接调用java、javac等命令。Windows 下的配置步骤大致是这样新建系统变量JAVA_HOME值填D:\dev\jdk\jdk-21然后在Path变量里新增一条%JAVA_HOME%\bin注意要把它移到其他 Java 相关路径的前面否则可能被旧版本覆盖。Linux/macOS 下则是在~/.bashrc或~/.zshrc里加两行export JAVA_HOME/opt/jdk/jdk-21 export PATH$JAVA_HOME/bin:$PATH配置完记得source一下配置文件然后开个新终端验证。2.4 验证安装是否成功验证不能只看java -version输出那个只能说明 PATH 配对了不能说明JAVA_HOME正确。完整的验证要跑三条命令java -version javac -version echo $JAVA_HOME # Windows 下用 echo %JAVA_HOME%三条命令的输出版本号必须一致都是 21。如果java -version是 21 但javac -version是 17说明 PATH 里有旧版本的 javac 排在前面得去调整顺序。如果JAVA_HOME输出为空或指向旧版本那 IDEA 里配置 SDK 时就会出问题。这一步千万别偷懒我见过太多“明明装了 21 但 IDEA 里就是认不到”的情况最后都是环境变量没配对。注意Windows 下修改环境变量后已经打开的终端和 IDEA 不会自动刷新必须重启它们才能读到新值。这个细节坑过不少人。3. 在 IDEA 2023.2 中配置 JDK 213.1 全局 SDK 与项目 SDK 的区别IDEA 里的 SDK 配置分两个层级全局 SDKPlatform Settings 里的 SDKs和项目 SDKProject Structure 里的 Project SDK。全局 SDK 是给整个 IDE 用的配置一次所有项目都能选项目 SDK 是给当前项目用的决定这个项目编译和运行时用哪个 JDK。很多人只配了全局 SDK结果项目里还是指向旧的就是因为没在项目层级再选一次。正确的做法是先在全局 SDK 里把 JDK 21 加进来然后在每个需要升级的项目里把 Project SDK 指向它。这样既统一了管理又能按项目灵活切换。如果你的项目用 Maven 或 Gradle还要注意构建工具自己的 JDK 配置它可能和 IDEA 的配置是分开的后面会细说。3.2 添加 JDK 21 到全局 SDK 列表打开 IDEA进File - Project Structure快捷键CtrlAltShiftS左侧选Platform Settings - SDKs。点左上角的号选Add JDK然后浏览到你 JDK 21 的安装目录比如D:\dev\jdk\jdk-21。选中后 IDEA 会自动识别版本号显示成21。给它起个清晰的名字比如jdk-21别用默认的21或者corretto-21这种方便后面区分。添加完成后你会在 SDK 列表里看到多个版本。这时候可以顺手检查一下每个 SDK 的Sourcepath和Documentation Path有没有配好配好了以后看 JDK 源码和文档会方便很多。不过这个不是必须的不影响编译运行。3.3 项目 SDK 与语言级别的对齐回到Project Structure的Project页面这里有两个关键下拉框SDK和Language level。SDK选刚加的jdk-21Language level选21 - Record patterns, pattern matching for switch。这两个必须对齐如果 SDK 选了 21 但语言级别还是 17那你写虚拟线程的代码时 IDEA 会提示语法不支持反过来语言级别选了 21 但 SDK 是 17编译时会报invalid target release: 21。这里有个容易忽略的点语言级别可以高于 SDK 版本但那样编译一定失败。IDEA 允许你这么选但不会阻止你所以配置完最好看一眼两个下拉框是不是都是 21。另外Project Structure里还有Modules页面每个模块也可以单独设语言级别如果模块级别和项目级别不一致以模块级别为准。多模块项目里这个坑很常见。3.4 编译器字节码版本的设置光配好 SDK 和语言级别还不够还得确认编译器的字节码版本。进Settings - Build, Execution, Deployment - Compiler - Java Compiler看看Project bytecode version是不是 21。如果这里是空的或者旧版本编译出来的 class 文件版本就不对运行时可能报Unsupported class file major version。字节码版本和语言级别的关系是这样的语言级别决定你能用哪些语法字节码版本决定编译产物能被哪个 JVM 加载。正常情况下两者应该一致都是 21。但有些特殊场景比如你想用 JDK 21 编译但让产物能在 JDK 17 上跑这叫交叉编译那语言级别和字节码版本可以设成 17不过这种场景很少见而且容易出兼容问题不建议新手尝试。3.5 Maven 和 Gradle 项目的额外配置如果你的项目是 Maven 或 Gradle 构建的光在 IDEA 里配还不够构建脚本里也得改否则命令行构建或者 CI 上会失败。Maven 项目要在pom.xml里配置maven-compiler-pluginplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source21/source target21/target release21/release /configuration /plugin这里推荐用release21/release而不是单独的source和target因为release参数会同时校验 API 和字节码版本更严格也更安全。Gradle 项目则在build.gradle里设置java { sourceCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_21 }或者用工具链的方式java { toolchain { languageVersion JavaLanguageVersion.of(21) } }工具链的好处是它会自动定位本地的 JDK 21不依赖环境变量团队协作时更稳。提示Maven 的maven-compiler-plugin版本要 3.11.0 以上Gradle 要 8.4 以上低版本对 JDK 21 支持不完整会在编译时报奇怪的错误。4. JDK 21 新特性实战与踩坑记录4.1 虚拟线程写法、调度与常见误区虚拟线程是 JDK 21 最受关注的特性它的核心价值在于用极低的资源开销支撑海量并发。传统平台线程Platform Thread一个线程对应一个操作系统线程创建成本高、内存占用大几千个就到顶了虚拟线程由 JVM 调度一个平台线程可以承载成千上万个虚拟线程特别适合 IO 密集型的场景比如处理大量 HTTP 请求、数据库查询等。创建虚拟线程的写法很直接Thread vThread Thread.ofVirtual().start(() - { System.out.println(运行在虚拟线程上: Thread.currentThread()); }); vThread.join();或者用Executors提供的工厂try (var executor Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10000).forEach(i - { executor.submit(() - { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); }这段代码创建了一万个任务每个任务睡一秒用虚拟线程跑几乎瞬间完成换成平台线程早就 OOM 了。但这里有几个坑必须说清楚。第一虚拟线程不适合 CPU 密集型任务因为它最终还是跑在平台线程上的CPU 密集场景用虚拟线程没有收益反而增加调度开销。第二不要在虚拟线程里用synchronized做长时间阻塞因为synchronized会 pin 住载体线程Carrier Thread导致其他虚拟线程无法调度这个叫“固定”Pinning。JDK 21 里应该用ReentrantLock替代。第三线程池不要复用虚拟线程虚拟线程的设计就是一次性的用完即弃用池化反而违背了它的设计初衷。注意Thread.sleep在虚拟线程里是可以正常让出载体线程的但synchronized块里的阻塞不行。如果你发现虚拟线程的吞吐量没达到预期先检查代码里有没有synchronized包裹的 IO 操作。4.2 Record Patterns 与模式匹配实战Record Patterns 让解构 record 变得非常优雅。以前要从一个 record 里取值得先判断类型再逐个调 accessor现在可以直接在instanceof或switch里解构record Point(int x, int y) {} record Line(Point start, Point end) {} Object obj new Line(new Point(0, 0), new Point(3, 4)); if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) { System.out.println(起点: x1 , y1 终点: x2 , y2); }嵌套解构也能一层层拆开代码可读性提升非常明显。配合switch的模式匹配可以写出很清晰的类型分发逻辑String describe(Object obj) { return switch (obj) { case Integer i when i 0 - 正整数: i; case Integer i - 非正整数: i; case String s - 字符串: s; case null - 空值; default - 其他类型; }; }这里有个细节要注意switch的模式匹配里null必须显式处理否则会抛NullPointerException。这是 JDK 21 的新行为和以前的switch不一样。另外when子句guard的顺序很重要范围窄的条件要写在前面否则会被宽条件先匹配掉。4.3 Sequenced Collections 的实用场景Sequenced Collections 引入了SequencedCollection、SequencedSet、SequencedMap三个接口给有序集合补上了统一的头尾操作。以前想拿List的第一个元素用get(0)拿Deque的第一个用getFirst()拿LinkedHashSet的第一个得用迭代器接口不统一。现在都统一成getFirst()、getLast()、addFirst()、addLast()、reversed()了。ListString list new ArrayList(List.of(a, b, c)); System.out.println(list.getFirst()); // a System.out.println(list.getLast()); // c System.out.println(list.reversed()); // [c, b, a]reversed()返回的是视图而不是拷贝对它的修改会反映到原集合上这个特性在做栈、队列操作时很方便。不过要注意reversed()返回的视图在并发修改时行为和原集合一致该加锁还是得加锁。4.4 分代 ZGC 的启用与观察分代 ZGC 是 JDK 21 里另一个重量级特性它把 ZGC 的低停顿优势扩展到了分代回收场景。启用方式很简单加启动参数-XX:UseZGC -XX:ZGenerational注意 JDK 21 里分代 ZGC 还需要显式加-XX:ZGenerational到了后续版本才默认开启。启用后可以用-Xlog:gc*观察 GC 日志看看停顿时间和吞吐量的变化。分代 ZGC 特别适合大堆内存几十 GB 以上且对停顿敏感的应用比如实时交易、在线游戏等。但如果你的堆很小几 GB用默认的 G1 可能更划算因为 ZGC 的额外开销在小堆上体现不出来。提示分代 ZGC 在 JDK 21 里还是相对新的特性生产环境启用前建议做充分的压测观察内存占用和 CPU 使用率的变化。5. 常见问题排查与避坑速查5.1 编译与运行时的版本类错误配置 JDK 21 过程中最常见的错误都和版本不匹配有关我把它们整理成一张速查表遇到问题直接对号入座。错误信息根本原因解决方法Unsupported class file major version 65运行时 JVM 版本低于 21加载不了 21 编译的 class确认运行配置里的 JRE 是 21检查JAVA_HOMEinvalid target release: 21编译器 JDK 低于 21检查 IDEA 的 Build Tools JDK 和 Maven/Gradle 的 JDKcannot find symbol: method ofVirtual()语言级别低于 21把 Project Language Level 调到 21package java.lang does not exist模块路径或 SDK 配置错误重新配置 Project SDK检查模块依赖class file has wrong version 65.0依赖库是用 21 编译的但项目用低版本 JDK统一项目 JDK 到 21或降级依赖库这张表里的错误我几乎都遇到过其中Unsupported class file major version 65出现频率最高十有八九是运行配置里的 JRE 没改。IDEA 的运行配置Run Configuration里有个单独的 JRE 选项默认可能是项目 SDK但如果你手动改过它就不会跟着项目 SDK 变得单独检查。5.2 IDEA 识别不到 JDK 21 的排查有时候明明装好了 JDK 21环境变量也配了但 IDEA 的 SDK 列表里就是找不到或者添加时报错。这种情况按下面的顺序排查先确认 IDEA 是不是从终端启动的如果是它可能继承了旧的环境变量重启 IDEA 试试再确认 JDK 安装目录下有没有release文件和bin/javac有些精简版 JDK 缺这些文件IDEA 认不出来最后检查 IDEA 的Build Tools - Maven - Runner里的 JRE 设置这里如果指向旧版本Maven 构建会用旧 JDK。还有一个隐蔽的坑IDEA 的缓存。有时候配置改对了但 IDEA 还是用旧值这时候进File - Invalidate Caches清一下缓存重启往往就好了。这个操作我每个月都要用几次尤其是切换 JDK 版本之后。5.3 虚拟线程调试的特殊注意事项虚拟线程的调试和普通线程不太一样。在 IDEA 的调试器里虚拟线程默认可能不会全部显示因为数量太多会拖垮调试器。IDEA 2023.2 对虚拟线程调试做了优化可以在调试窗口里过滤和折叠。但要注意在虚拟线程里打断点可能会影响调度行为因为断点会让线程暂停进而影响载体线程的调度。调试虚拟线程相关代码时建议用日志而不是断点或者只在关键路径上打断点。另外虚拟线程的堆栈跟踪和平台线程不同它显示的是逻辑调用栈不包含载体线程的信息。如果你需要看虚拟线程跑在哪个载体线程上得用Thread.currentThread()打印或者用 JFRJava Flight Recorder事件来观察。JFR 对虚拟线程的支持在 JDK 21 里已经比较完善了jdk.VirtualThreadStart和jdk.VirtualThreadEnd事件可以帮你分析虚拟线程的生命周期。5.4 多模块项目的版本一致性维护多模块项目里最容易出的问题是各个模块的 JDK 版本不一致。父 POM 里配了 21但某个子模块的pom.xml里又写了 17构建时就会混乱。我的做法是在父 POM 里用properties统一定义版本子模块引用属性而不是写死properties maven.compiler.release21/maven.compiler.release java.version21/java.version /propertiesGradle 多模块项目则可以在根build.gradle里用subprojects或allprojects统一配置或者用java-toolchain插件在每个模块里声明工具链。关键是只在一个地方定义版本其他地方引用避免散落各处导致不一致。提示IDEA 的Modules页面里每个模块的语言级别也要检查一遍有时候 Maven 重新导入后模块级别会被重置成默认值导致编译报错。6. 从 JDK 17 平滑迁移到 21 的实操建议6.1 迁移前的依赖兼容性扫描从 JDK 17 迁到 21大部分代码不用改但依赖库需要过一遍。重点检查这几类字节码操作库ASM、ByteBuddy、CGLIB、反射框架Spring、Hibernate 的某些版本、序列化库Jackson、Kryo。这些库如果版本太老可能不认识 major version 65 的 class 文件或者用了 JDK 21 里被移除的内部 API。扫描方法很简单先把项目 JDK 切到 21 编译一遍看有没有编译错误然后跑一遍完整的测试套件看运行时有没有异常。如果某个依赖报错先查它的最新版本有没有支持 JDK 21有就升级没有就找替代方案。Spring Boot 3.x 对 JDK 21 支持很好如果还在用 Spring Boot 2.x迁移 JDK 21 之前最好先升到 3.x。6.2 分阶段迁移的节奏控制一次性把所有模块都升到 21 风险太大建议分阶段来。第一阶段只升级本地开发环境的 JDK 和 IDEA 配置代码不动验证编译和测试能过第二阶段把构建脚本里的版本号改成 21在 CI 上跑一遍确认构建产物没问题第三阶段逐步在新代码里使用 JDK 21 的新特性老代码保持不动第四阶段等新特性用稳了再回头重构老代码。这个节奏的好处是每一步都可回退出问题能快速定位是哪个环节引入的。我见过有人一步到位全升结果一个依赖库不兼容整个项目卡住好几天。6.3 团队协作中的版本约定团队里每个人的环境不一样光靠口头约定“大家都用 21”是不够的。我的做法是在项目根目录放一个.java-version文件配合版本管理工具或者用 Maven/Gradle 的工具链配置强制指定版本。工具链的好处是它会自动下载或定位指定版本的 JDK不依赖开发者本地装了什么CI 上也能保证一致。另外在 README 或者贡献指南里写清楚 JDK 版本要求、构建命令、常见问题新成员上手时能少踩很多坑。这些文档看起来不起眼但团队规模一大省下的沟通成本非常可观。6.4 回退方案与风险控制升级过程中一定要留好回退路径。最直接的做法是在版本控制里开一个升级分支主分支保持不动等升级分支验证稳定了再合并。如果项目已经用了容器化部署可以在 Dockerfile 里固定 JDK 版本回退时改一行就行。还有一点容易被忽略数据库驱动、消息队列客户端这类和运行时环境强相关的依赖升级 JDK 后要重点测试。它们可能用了 JDK 内部 API或者对字节码版本敏感。我遇到过一次升级 JDK 21 后消息队列客户端连接异常排查半天发现是客户端依赖的一个底层库版本太老升级后就好了。7. 我个人的配置习惯与效率技巧7.1 用 SDK 命名规范减少切换成本我在 IDEA 里给每个 JDK 都起了带版本和发行版的名字比如temurin-17、temurin-21、corretto-21而不是默认的17、21。这样在项目里切换 SDK 时一眼就能看出用的是哪个发行版避免“这个 21 到底是哪个 21”的困惑。多项目并行开发时这个习惯能省不少事。另外我会把常用的 JDK 路径记在一个笔记里配置新机器或者帮同事排查时直接复制不用每次去翻安装目录。这种小习惯积累起来效率提升很明显。7.2 快捷键与配置模板的复用IDEA 的Project Structure对话框可以用CtrlAltShiftS快速打开配置 JDK 时我基本不点菜单。语言级别和字节码版本的设置我会在新建项目时就配好然后导出成项目模板下次新建直接套用不用重复配置。对于 Maven 项目我会把maven-compiler-plugin的配置写进一个公司内部的父 POM所有项目继承这样新项目天然就是 JDK 21 配置不用每个项目单独改。这个做法在团队里推广后配置相关的求助少了一大半。7.3 保持 IDEA 与 JDK 版本同步更新JDK 和 IDEA 都在持续更新小版本里往往包含重要的兼容性修复。我的习惯是每季度检查一次 IDEA 和 JDK 的小版本更新在测试环境验证后升级。特别是 JDK 的补丁版本安全修复和性能优化都值得跟进。但要注意不要在生产环境直接用最新版本等它稳定一两个补丁版本再上这是血泪教训。最后分享一个小技巧如果你不确定某个新特性在当前配置下能不能用可以写一个最小的测试类用jshell直接跑一下。jshell是 JDK 自带的交互式工具启动快、验证方便比在 IDEA 里建项目试要高效得多。配置 JDK 21 后jshell也会跟着用 21直接在里面敲虚拟线程的代码就能验证环境对不对。