Skip to content

JDK 新特性与升级实践 ​

相关笔记:03 Java 高级特性(Lambda / Stream / 虚拟线程入门)、JVM 深入(GC 与参数)

03 篇讲的是「语法糖怎么用」;这篇讲版本路线、落地取舍、升级踩坑。面试问「JDK 21 有什么新特性」时,能答出「所以我升不升、升了要注意什么」,比背 JEP 编号有用得多。

1. 版本节奏:为什么大家都在追 LTS ​

2017 年之后,JDK 变成 6 个月一个 feature 版本,每 2 年一个 LTS:

版本发布LTS现状建议
82014✅大量存量系统仍在此,无官方免费更新(Oracle 收费,可用 Temurin/Corretto)
112018✅保守升级的落点,仍有免费更新
172021✅当前主流基线,Spring Boot 3 最低要求
212023✅新项目首选,虚拟线程正式版
252025✅下一代 LTS,逐步进入视野

关键分水岭:Spring Boot 3 要求 JDK 17+,Spring Framework 6 也是。所以「还在 JDK 8」的项目,升级链路通常是 8 → 17 → 21,中间不跳 11 也行(但 11 更稳、依赖兼容更好)。

2. JDK 9 ~ 16:语法与基础设施 ​

2.1 JDK 9:模块化 + 集合工厂 ​

java
// 模块化: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 与文本块 ​

java
var list = new ArrayList<String>();     // 局部变量类型推断,只在编译期,不是动态类型
var list = new ArrayList<String>();     // 局部变量类型推断,只在编译期,不是动态类型

var 的三条限制:只能用于局部变量(含 for/增强 for)、必须初始化、不能为 null(推不出类型)。

java
// JDK 13 预览、15 正式
String json = """
        {"name": "张三", "age": 18}
        """;
// JDK 13 预览、15 正式
String json = """
        {"name": "张三", "age": 18}
        """;

2.3 JDK 11:HttpClient 转正 ​

java
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 模式 ​

java
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) ​

java
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) ​

java
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,正式) ​

java
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、锁等待)时,虚拟线程把栈冻结到堆上、让出载体线程,恢复时再挂回来。所以「阻塞」不再等于「占线程」。

三条铁律:

  1. 不要池化虚拟线程。它创建成本极低(几十字节 + 堆上栈),池化反而丢掉了「用完即弃」的优势。用 newVirtualThreadPerTaskExecutor 或 Thread.ofVirtual().start()。
  2. 别用 ThreadLocal 存大对象(百万虚拟线程 = 百万份副本),改用 ScopedValue(JDK 21 预览)。
  3. 注意 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 里任务泄漏、异常丢失、取消不传播的问题。

java
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 模式匹配(正式) ​

java
// 嵌套解构:直接取出 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 分代) / Shenandoah
Serial / Parallel / CMS(已移除) / G1(默认, JDK9+) / ZGC(11+,21 分代) / Shenandoah
收集器停顿适用
Parallel较大吞吐优先、批处理
G1200ms 级可控默认选择,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=permit17 起无效,改 --add-opens
-Xlog:gc*是统一日志(9+),旧的 -XX:+PrintGCDetails 已废

6.3 升级方法论(能讲出来就很加分) ​

  1. 先升依赖再升 JDK:框架版本(Spring Boot)往往比 JDK 更卡;
  2. CI 里加多版本矩阵:8/17/21 同时跑测试,灰度切换;
  3. 先 17 再 21,不要 8 直接跳 21(两个大步的问题会混在一起);
  4. 用 jdeps 扫内部 API 依赖:
bash
jdeps --jdk-internals -R target/app.jar
jdeps --jdk-internals -R target/app.jar
  1. -XX:+PrintFlagsFinal 对比参数默认值(升级后默认 GC、堆比例都可能变);
  2. 压测对比:GC 日志、P99、内存占用、启动时间四项。

6.4 升级到 21 的收益怎么量化 ​

指标8 → 21 的典型变化(因系统而异,要实测)
吞吐G1 + 更好的 JIT,普遍提升 5%~20%
停顿CMS 消失,G1 停顿更可控
内存Compact Strings 省 10%~20% 字符串内存
并发能力开虚拟线程后,IO 密集型服务的线程数天花板基本消失
启动AppCDS 默认,快一些;GraalVM 原生镜像更快(另一条路)

7. 面试怎么答 ​

被问「JDK 21 有什么新特性」时,按这个结构答:

  1. 一句话定位:21 是 LTS,最大的变化是虚拟线程转正。
  2. 原理:虚拟线程由 JVM 调度、复用车载平台线程,阻塞时把栈冻结到堆,所以「阻塞不再占线程」。
  3. 用法与禁忌:不要池化;synchronized 里阻塞会 pinning,改用 ReentrantLock;ThreadLocal 换 ScopedValue。
  4. 其他:分代 ZGC、记录模式 + switch 模式匹配(配合 sealed 能穷尽检查)、顺序集合。
  5. 落地判断:IO 密集 + 传统每请求一线程的架构收益最大;已经是 WebFlux 收益有限;CPU 密集没用。
  6. 升级注意事项:Spring Boot 3 最低 17;synchronized pinning 要排查;--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 未修复)
默认 GCJDK 9+ 是 G1
升级最大坑强封装(--add-opens)、JAXB 移除、CMS 移除、javax→jakarta