JDK 新特性与升级实践
相关笔记:03 Java 高级特性(Lambda / Stream / 虚拟线程入门)、JVM 深入(GC 与参数)
03 篇讲的是「语法糖怎么用」;这篇讲版本路线、落地取舍、升级踩坑。面试问「JDK 21 有什么新特性」时,能答出「所以我升不升、升了要注意什么」,比背 JEP 编号有用得多。
1. 版本节奏:为什么大家都在追 LTS
2017 年之后,JDK 变成 6 个月一个 feature 版本,每 2 年一个 LTS:
| 版本 | 发布 | LTS | 现状建议 |
|---|---|---|---|
| 8 | 2014 | ✅ | 大量存量系统仍在此,无官方免费更新(Oracle 收费,可用 Temurin/Corretto) |
| 11 | 2018 | ✅ | 保守升级的落点,仍有免费更新 |
| 17 | 2021 | ✅ | 当前主流基线,Spring Boot 3 最低要求 |
| 21 | 2023 | ✅ | 新项目首选,虚拟线程正式版 |
| 25 | 2025 | ✅ | 下一代 LTS,逐步进入视野 |
关键分水岭:Spring Boot 3 要求 JDK 17+,Spring Framework 6 也是。所以「还在 JDK 8」的项目,升级链路通常是 8 → 17 → 21,中间不跳 11 也行(但 11 更稳、依赖兼容更好)。
2. JDK 9 ~ 16:语法与基础设施
2.1 JDK 9:模块化 + 集合工厂
// 模块化:module-info.java
module com.example.order {
requires java.sql;
exports com.example.order.api;
}
// 不可变集合工厂
List.of(1, 2, 3);
Set.of("a", "b");
Map.of("k1", 1, "k2", 2);// 模块化:module-info.java
module com.example.order {
requires java.sql;
exports com.example.order.api;
}
// 不可变集合工厂
List.of(1, 2, 3);
Set.of("a", "b");
Map.of("k1", 1, "k2", 2);同时 9 还带来:G1 成为默认 GC、jshell、Optional.ifPresentOrElse、Stream.takeWhile/dropWhile、InputStream.transferTo、Compact Strings(String 底层由 char[] 改 byte[],省一半内存)。
2.2 JDK 10 ~ 12:var 与文本块
var list = new ArrayList<String>(); // 局部变量类型推断,只在编译期,不是动态类型var list = new ArrayList<String>(); // 局部变量类型推断,只在编译期,不是动态类型var 的三条限制:只能用于局部变量(含 for/增强 for)、必须初始化、不能为 null(推不出类型)。
// JDK 13 预览、15 正式
String json = """
{"name": "张三", "age": 18}
""";// JDK 13 预览、15 正式
String json = """
{"name": "张三", "age": 18}
""";2.3 JDK 11:HttpClient 转正
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
HttpResponse<String> resp = client.send(
HttpRequest.newBuilder(URI.create("https://api.example.com")).GET().build(),
HttpResponse.BodyHandlers.ofString());HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();
HttpResponse<String> resp = client.send(
HttpRequest.newBuilder(URI.create("https://api.example.com")).GET().build(),
HttpResponse.BodyHandlers.ofString());顺带:String.strip()/isBlank()/repeat()/lines()、单文件源码直接运行(java Hello.java)、ZGC 实验性引入、TLS 1.3。
2.4 JDK 14 ~ 16:record 与 instanceof 模式
public record Point(int x, int y) {} // 自动生成构造、getter、equals、hashCode、toString
if (obj instanceof String s && s.length() > 3) { // 模式匹配,省一次强转
System.out.println(s);
}public record Point(int x, int y) {} // 自动生成构造、getter、equals、hashCode、toString
if (obj instanceof String s && s.length() > 3) { // 模式匹配,省一次强转
System.out.println(s);
}record 是不可变数据载体,适合 DTO / 值对象 / 多返回值的替代;它不是 JavaBean,不能继承,也不该拿来做 ORM 实体(JPA 要无参构造)。
2.5 Switch 表达式(JDK 14)
String type = switch (code) {
case 1, 2 -> "小额";
case 3 -> { yield "中额"; } // 多语句用 yield 返回值
default -> throw new IllegalArgumentException();
};String type = switch (code) {
case 1, 2 -> "小额";
case 3 -> { yield "中额"; } // 多语句用 yield 返回值
default -> throw new IllegalArgumentException();
};对比老 switch:有返回值、不穿透、更严格(必须穷尽或 default)。
3. JDK 17 LTS:密封类与强封装
3.1 密封类(sealed)
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}配合 switch 模式匹配,编译器能检查是否穷尽,这是「代数数据类型」进入 Java 的标志。
3.2 强封装(最重要的升级障碍)
JDK 16 起默认强封装 JDK 内部 API,JDK 17 彻底移除 --illegal-access。后果:
- 反射访问
sun.misc.*、jdk.internal.*会抛InaccessibleObjectException; - 需要显式
--add-opens java.base/java.lang=ALL-UNNAMED; - 大量老框架(老版本 Spring、Dubbo、Hibernate、Lombok 早期版本)要靠这个参数才能跑。
3.3 其他
- 移除 CMS(JDK 14),
-XX:+UseConcMarkSweepGC直接报错; - 移除 Nashorn(JDK 15)、移除 JAXB / JAX-WS(JDK 11,迁到 Jakarta);
- 弃用 Security Manager(JDK 17 标记
forRemoval); - 引入伪随机数新接口
RandomGenerator、AppCDS默认开启、外部函数 API 预览。
4. JDK 21 LTS:虚拟线程是主角
4.1 虚拟线程(JEP 444,正式)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 阻塞时自动让出载体线程
return "ok";
});
}
}try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(1000); // 阻塞时自动让出载体线程
return "ok";
});
}
}原理:虚拟线程由 JVM 调度,多个虚拟线程复用一个平台线程(载体线程)。遇到阻塞(sleep、IO、锁等待)时,虚拟线程把栈冻结到堆上、让出载体线程,恢复时再挂回来。所以「阻塞」不再等于「占线程」。
三条铁律:
- 不要池化虚拟线程。它创建成本极低(几十字节 + 堆上栈),池化反而丢掉了「用完即弃」的优势。用
newVirtualThreadPerTaskExecutor或Thread.ofVirtual().start()。 - 别用
ThreadLocal存大对象(百万虚拟线程 = 百万份副本),改用 ScopedValue(JDK 21 预览)。 - 注意 pinning(钉住):在
synchronized块里阻塞,会让载体线程被钉住,无法切换。JDK 21 上这是真问题(JDK 24 的 JEP 491 才修好)。所以在虚拟线程里优先用ReentrantLock而不是synchronized。
什么时候有用:IO 密集、高并发、每请求一线程的传统写法(Spring MVC + Tomcat 每请求一线程)。一个 8 核机器从「几千线程」变成「几十万虚拟线程」。
什么时候没用:CPU 密集型(瓶颈是核数,不是线程数);已经在用 WebFlux/Reactor 的场景(本身就不占线程)。
Spring Boot 3.2 起只需一行
spring.threads.virtual.enabled=true就能让 Tomcat 用上虚拟线程。
4.2 结构化并发(预览,JEP 453)
把「一组子任务」当一个整体:一起开始、一起结束、出错一起取消,解决 ExecutorService 里任务泄漏、异常丢失、取消不传播的问题。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> userService.get(id));
var order = scope.fork(() -> orderService.list(id));
scope.join().throwIfFailed();
return new Detail(user.get(), order.get());
}try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> userService.get(id));
var order = scope.fork(() -> orderService.list(id));
scope.join().throwIfFailed();
return new Detail(user.get(), order.get());
}4.3 记录模式与 switch 模式匹配(正式)
// 嵌套解构:直接取出 record 里的字段
if (obj instanceof Point(int x, int y)) { ... }
// 穷尽性检查 + null 处理
String desc = switch (shape) {
case Circle(double r) -> "圆 r=" + r;
case Square(double s) -> "方 s=" + s;
case Triangle(double a, double b, double c) -> "三角";
};// 嵌套解构:直接取出 record 里的字段
if (obj instanceof Point(int x, int y)) { ... }
// 穷尽性检查 + null 处理
String desc = switch (shape) {
case Circle(double r) -> "圆 r=" + r;
case Square(double s) -> "方 s=" + s;
case Triangle(double a, double b, double c) -> "三角";
};4.4 其他 JDK 21 变化
| 特性 | 状态 | 说明 |
|---|---|---|
| 分代 ZGC | 正式 | 低延迟 + 吞吐兼顾,大堆首选 |
| Sequenced Collection | 正式 | List.getFirst()/getLast()/reversed(),统一首尾操作 |
字符串模板 STR."..." | 预览 | 安全的字符串插值(后续版本又被收回重做,注意) |
| ScopedValue | 预览 | 替代 ThreadLocal,适配虚拟线程 |
| 外部函数与内存 API | 预览 | 替代 JNI |
| Vector API | 孵化 | SIMD 计算 |
5. GC 演进
Serial / Parallel / CMS(已移除) / G1(默认, JDK9+) / ZGC(11+,21 分代) / ShenandoahSerial / Parallel / CMS(已移除) / G1(默认, JDK9+) / ZGC(11+,21 分代) / Shenandoah| 收集器 | 停顿 | 适用 |
|---|---|---|
| Parallel | 较大 | 吞吐优先、批处理 |
| G1 | 200ms 级可控 | 默认选择,4~32GB 堆 |
| ZGC | < 1ms | 大堆(几十上百 GB)、低延迟要求 |
| Shenandoah | 低 | 与 ZGC 类似,OpenJDK 系 |
口径:默认就用 G1,别乱调。堆超过 32GB 且对延迟敏感,再考虑 ZGC(-XX:+UseZGC -XX:+ZGenerational)。
GC 细节、ZGC 染色指针、调优参数见 JVM 深入。
6. 升级实战:从 8 到 17/21 会遇到什么
6.1 依赖与包
| 问题 | 表现 | 处理 |
|---|---|---|
| JAXB / JAX-WS 被移除 | javax.xml.bind.* 找不到 | 显式引入 jakarta 依赖 |
| Nashorn 被移除 | ScriptEngine 拿不到 js | 换 GraalJS |
sun.misc.Unsafe 受限 | 警告/后续不可用 | 换 VarHandle / MethodHandles |
| 第三方库反射 JDK 内部 | InaccessibleObjectException | --add-opens(先查依赖版本) |
| javax → jakarta | 包名全变 | Spring Boot 3 必须迁(javax.* → jakarta.*) |
6.2 JVM 参数变化
| 旧参数 | 现状 |
|---|---|
-XX:+UseConcMarkSweepGC | 报错(14 起移除),改 -XX:+UseG1GC |
-XX:PermSize/-XX:MaxPermSize | 早就无效,改 -XX:MetaspaceSize |
--illegal-access=permit | 17 起无效,改 --add-opens |
-Xlog:gc* | 是统一日志(9+),旧的 -XX:+PrintGCDetails 已废 |
6.3 升级方法论(能讲出来就很加分)
- 先升依赖再升 JDK:框架版本(Spring Boot)往往比 JDK 更卡;
- CI 里加多版本矩阵:8/17/21 同时跑测试,灰度切换;
- 先 17 再 21,不要 8 直接跳 21(两个大步的问题会混在一起);
- 用
jdeps扫内部 API 依赖:
jdeps --jdk-internals -R target/app.jarjdeps --jdk-internals -R target/app.jar-XX:+PrintFlagsFinal对比参数默认值(升级后默认 GC、堆比例都可能变);- 压测对比:GC 日志、P99、内存占用、启动时间四项。
6.4 升级到 21 的收益怎么量化
| 指标 | 8 → 21 的典型变化(因系统而异,要实测) |
|---|---|
| 吞吐 | G1 + 更好的 JIT,普遍提升 5%~20% |
| 停顿 | CMS 消失,G1 停顿更可控 |
| 内存 | Compact Strings 省 10%~20% 字符串内存 |
| 并发能力 | 开虚拟线程后,IO 密集型服务的线程数天花板基本消失 |
| 启动 | AppCDS 默认,快一些;GraalVM 原生镜像更快(另一条路) |
7. 面试怎么答
被问「JDK 21 有什么新特性」时,按这个结构答:
- 一句话定位:21 是 LTS,最大的变化是虚拟线程转正。
- 原理:虚拟线程由 JVM 调度、复用车载平台线程,阻塞时把栈冻结到堆,所以「阻塞不再占线程」。
- 用法与禁忌:不要池化;
synchronized里阻塞会 pinning,改用ReentrantLock;ThreadLocal换ScopedValue。 - 其他:分代 ZGC、记录模式 + switch 模式匹配(配合 sealed 能穷尽检查)、顺序集合。
- 落地判断:IO 密集 + 传统每请求一线程的架构收益最大;已经是 WebFlux 收益有限;CPU 密集没用。
- 升级注意事项:Spring Boot 3 最低 17;
synchronizedpinning 要排查;--add-opens要补。
8. 速查表
| 问题 | 一句话 |
|---|---|
| LTS 有哪几个 | 8、11、17、21、25 |
| Spring Boot 3 要求 | JDK 17+ |
var 能用在哪 | 只能局部变量,必须初始化,不能 null |
| record 干什么 | 不可变数据载体,自动生成 equals/hashCode/toString,不能继承 |
| sealed 干什么 | 限定实现类,配合 switch 能穷尽检查 |
| 虚拟线程原理 | JVM 调度,阻塞时栈冻结到堆、让出载体线程 |
| 虚拟线程能用池吗 | 不能,用完即弃 |
| pinning 是什么 | 在 synchronized 里阻塞会钉住载体线程(21 未修复) |
| 默认 GC | JDK 9+ 是 G1 |
| 升级最大坑 | 强封装(--add-opens)、JAXB 移除、CMS 移除、javax→jakarta |
Cao Note