Java 21 到 Java 26 新特性详解:虚拟线程、模式匹配、结构化并发、HTTP/3 与 AOT
从 Java 21 到 Java 26,Java 平台经历的并不是一次简单的语法升级,而是一轮覆盖并发模型、语言表达能力、启动性能、网络协议、垃圾收集器、安全边界与开发体验的系统演进。
这几个版本中,最值得关注的五条主线是:
- Java 21 正式交付虚拟线程,让"一个请求一个线程"的同步编程模型重新具备高并发扩展能力。
- Java 21 正式交付记录模式与
switch模式匹配,Java 的数据检查与解构能力趋于统一。 - 结构化并发从 Java 21 一直预览到 Java 26,API 在 Java 25 和 Java 26 中发生了重要变化。
- Java 26 的标准 HTTP Client 正式支持 HTTP/3,但默认协议仍然是 HTTP/2。
- Project Leyden 从 Java 24 开始逐步交付 AOT 缓存能力,持续改善 JVM 的启动和预热速度。
本文基于截至 2026 年 8 月 27 日已经正式发布的 JDK 21---26 编写。文中会明确标注"正式""预览""孵化器"和"实验性"状态,避免把尚未定版的 API 当成稳定能力。
1. Java 21 到 Java 26 版本全景
1.1 发布时间与版本定位
| Java 版本 | GA 日期 | Class 文件主版本 | JEP 数量 | 版本定位 |
|---|---|---|---|---|
| Java 21 | 2023-09-19 | 65 | 15 | 多数主流厂商提供 LTS,虚拟线程与模式匹配正式落地 |
| Java 22 | 2024-03-19 | 66 | 12 | FFM API、未命名变量正式,多个 API 继续预览 |
| Java 23 | 2024-09-17 | 67 | 12 | 原始类型模式匹配首预览,ZGC 默认使用分代模式 |
| Java 24 | 2025-03-18 | 68 | 24 | AOT、Stream Gatherers、Class-File API 与虚拟线程改进集中交付 |
| Java 25 | 2025-09-16 | 69 | 18 | 多数主流厂商提供 LTS,Scoped Values 与多项语言特性正式定版 |
| Java 26 | 2026-03-17 | 70 | 10 | HTTP/3 正式落地,AOT 支持任意 GC,结构化并发第六次预览 |
需要注意,LTS 是 JDK 发行商的长期支持策略,不是 Java 语言规范中的关键字。Oracle、Eclipse Temurin、Amazon Corretto、Azul Zulu 等发行版的支持周期和商业条款可能不同,生产选型时应以实际采用的发行版为准。
1.2 六个版本最值得关注的能力
| 能力 | 首次出现在本文范围 | 当前状态(Java 26) | 主要价值 |
|---|---|---|---|
| 虚拟线程 | Java 21 正式 | 正式 | 用同步阻塞代码支撑大量 I/O 并发任务 |
| 记录模式 | Java 21 正式 | 正式 | 直接解构 record,减少类型转换和样板代码 |
switch 模式匹配 |
Java 21 正式 | 正式 | 类型匹配、守卫条件、穷举检查统一进入 switch |
| 原始类型模式匹配 | Java 23 预览 | Java 26 第四次预览 | 让 instanceof、模式和 switch 支持所有基本类型 |
| 结构化并发 | Java 21 预览 | Java 26 第六次预览 | 把一组并发子任务作为一个生命周期单元管理 |
| Scoped Values | Java 21 预览 | Java 25 起正式 | 在线程调用链中安全、高效地传递只读上下文 |
| FFM API | Java 21 第三次预览 | Java 22 起正式 | 用标准 API 调用本地函数和访问堆外内存 |
| Stream Gatherers | Java 22 预览 | Java 24 起正式 | 为 Stream 增加窗口、折叠等自定义中间操作 |
| Class-File API | Java 22 预览 | Java 24 起正式 | 标准化 class 文件解析、生成和转换 |
| AOT 缓存 | Java 24 | Java 26 持续增强 | 改善启动时间和到达峰值性能前的预热时间 |
| HTTP/3 | Java 26 | 正式 | 标准 HTTP Client 可通过 QUIC 访问 HTTP/3 服务 |
2. 先分清正式、预览、孵化器和实验性特性
阅读新版 Java 特性时,最容易踩的坑不是语法,而是误判特性的稳定状态。
| 状态 | 含义 | 是否需要额外参数 | 生产建议 |
|---|---|---|---|
| 正式特性 | 已进入 Java SE 或 JDK 正式能力,兼容性承诺最强 | 不需要 | 可以正常评估并投入生产 |
| Preview | 设计已较完整,但仍可能变更或撤回 | 编译、运行都要 --enable-preview |
适合试点,不宜暴露为长期公共 API |
| Incubator | 仍处于孵化阶段,通常位于 jdk.incubator.* 模块 |
通常需要 --add-modules |
适合实验、压测和反馈 |
| Experimental | JVM 或运行时实验能力 | 通常需要专用 JVM 参数 | 必须经过专项验证,不应默认开启 |
以 Java 26 的预览代码为例:
bash
javac --enable-preview --release 26 Demo.java
java --enable-preview Demo
预览特性还有两个重要限制:
- 预览 class 文件与具体 Java 版本绑定。例如使用 Java 25 预览特性编译的代码,不能把"预览兼容"理所当然地延续到 Java 26。
- 预览特性可能改变甚至撤回。Java 21 和 Java 22 预览过的 String Templates 后来没有进入 Java 23,相关第三次预览 JEP 465 已被撤回。
因此,业务代码使用预览 API 时,最好集中封装在少量模块中,不要扩散到公共 SDK、领域对象和跨服务接口。
3. Java 21:虚拟线程与模式匹配正式落地
Java 21 是这一轮演进的起点,也是很多企业从 Java 8、11 或 17 升级时选择的长期运行基线。
3.1 Java 21 的 15 个 JEP
| 分类 | JEP 与特性 | 状态 |
|---|---|---|
| 并发 | JEP 444:Virtual Threads | 正式 |
| 并发 | JEP 446:Scoped Values | 预览 |
| 并发 | JEP 453:Structured Concurrency | 预览 |
| 语言 | JEP 440:Record Patterns | 正式 |
| 语言 | JEP 441:Pattern Matching for switch |
正式 |
| 语言 | JEP 443:Unnamed Patterns and Variables | 预览 |
| 语言 | JEP 445:Unnamed Classes and Instance Main Methods | 预览 |
| 语言/API | JEP 430:String Templates | 预览,后续未定版 |
| 集合 | JEP 431:Sequenced Collections | 正式 |
| 内存与本地互操作 | JEP 442:Foreign Function & Memory API | 第三次预览 |
| GC | JEP 439:Generational ZGC | 正式 |
| 向量计算 | JEP 448:Vector API | 第六次孵化 |
| 安全 | JEP 452:Key Encapsulation Mechanism API | 正式 |
| 工具与兼容性 | JEP 451:准备禁止动态加载 Agent | 正式行为变更 |
| 平台清理 | JEP 449:弃用 Windows 32 位 x86 端口并准备移除 | 弃用 |
3.2 Sequenced Collections:统一集合的首尾操作
Java 21 引入 SequencedCollection、SequencedSet 和 SequencedMap,为具有确定遍历顺序的集合补齐统一的首尾操作和反向视图。
java
import java.util.ArrayList;
import java.util.List;
public class SequencedCollectionDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("B", "C"));
names.addFirst("A");
names.addLast("D");
System.out.println(names.getFirst()); // A
System.out.println(names.getLast()); // D
System.out.println(names.reversed()); // [D, C, B, A]
}
}
reversed() 返回的是反向视图,不是一次无条件的完整复制。修改原集合或可修改视图时,需要理解二者之间的关联。
3.3 Generational ZGC
JEP 439 为 ZGC 引入分代模式。它利用"大多数对象朝生夕灭"的代际假设,把年轻对象与长期存活对象区别处理,在保持低停顿目标的同时减少不必要的扫描工作。
Java 21 中可以通过下面的参数启用分代 ZGC:
bash
java -XX:+UseZGC -XX:+ZGenerational -jar app.jar
到了 Java 23,ZGC 默认使用分代模式;到了 Java 24,非分代模式被移除。因此升级时不能只复制旧 GC 参数,还要重新核对参数是否仍然有效。
4. 虚拟线程详解:Java 21 并发模型的核心变化
4.1 平台线程为什么昂贵
传统平台线程通常与操作系统线程一一对应。操作系统线程需要栈空间、内核调度和上下文切换,数量过多时会带来明显成本。
因此,过去的 Java 服务常使用固定线程池:
java
var executor = java.util.concurrent.Executors.newFixedThreadPool(200);
线程池解决的是"平台线程稀缺"问题,但也带来新的工程问题:
- 线程数过少时,请求排队,吞吐受限。
- 线程数过多时,内存和调度成本上升。
- 异步链路、回调和
CompletableFuture组合会增加异常处理与调试难度。 - 线程池大小经常被错误地当作下游服务的并发限流器。
4.2 虚拟线程是什么
虚拟线程仍然是 java.lang.Thread,但由 JVM 调度,不会在整个生命周期中独占某个操作系统线程。
虚拟线程执行 Java 代码时,会挂载到一个平台线程上,这个平台线程称为 carrier thread。遇到 JDK 能识别的阻塞 I/O 时,虚拟线程可以卸载,carrier 随即去执行其他虚拟线程。I/O 就绪后,原虚拟线程再挂载并继续执行。
可以把它理解为:
text
大量业务虚拟线程
↓ JVM 调度与挂载
少量 carrier 平台线程
↓ OS 调度
CPU 核心
虚拟线程的核心价值是提高大量阻塞任务下的吞吐量,而不是让单个任务执行得更快。
4.3 创建虚拟线程
直接创建一个虚拟线程:
java
public class VirtualThreadStartDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.ofVirtual()
.name("order-query")
.start(() -> System.out.println(Thread.currentThread()));
thread.join();
}
}
为每个任务创建一个虚拟线程:
java
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
public class VirtualThreadExecutorDemo {
public static void main(String[] args) throws Exception {
List<Callable<String>> tasks = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
int taskId = i;
tasks.add(() -> {
Thread.sleep(Duration.ofMillis(100));
return "task-" + taskId + " -> " + Thread.currentThread();
});
}
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = executor.invokeAll(tasks);
System.out.println(futures.getFirst().get());
System.out.println("任务数:" + futures.size());
}
}
}
newVirtualThreadPerTaskExecutor() 不是一个固定大小的虚拟线程池。它为每个任务创建新的虚拟线程,这正是推荐的使用方式。
4.4 虚拟线程适合什么场景
适合:
- 大量并发 HTTP/RPC 请求。
- JDBC、Redis、消息队列等阻塞式客户端调用。
- 文件和网络 I/O。
- 请求生命周期清晰的 thread-per-request 服务。
- 希望保留同步代码、异常栈和传统调试体验的系统。
不一定适合:
- 长时间占用 CPU 的计算任务。
- 已经是完整事件循环模型、且没有阻塞调用的系统。
- 依赖大量昂贵
ThreadLocal缓存的旧框架。 - 下游容量很小,却没有并发隔离和背压的调用链。
虚拟线程不会增加 CPU 核心,也不会让 CPU 密集型计算自动并行得更快。CPU 密集型任务仍应根据核心数控制并行度。
4.5 不要用线程池给虚拟线程限流
如果某个下游最多只能承受 50 个并发请求,应使用 Semaphore 表达资源上限,而不是把虚拟线程塞进固定线程池。
java
import java.util.concurrent.Semaphore;
public class LimitedRemoteService {
private final Semaphore permits = new Semaphore(50);
public String call() throws InterruptedException {
permits.acquire();
try {
return invokeRemoteService();
} finally {
permits.release();
}
}
private String invokeRemoteService() throws InterruptedException {
Thread.sleep(100);
return "ok";
}
}
数据库连接池本身就具有类似信号量的容量约束,通常不需要再在连接池外机械叠加一个相同上限的信号量。
4.6 Pinning:从 Java 21 到 Java 26 的变化
在 Java 21 中,虚拟线程如果在 synchronized 代码块内执行长时间阻塞操作,可能被固定在 carrier 上,导致 carrier 无法去执行其他虚拟线程。
Java 21 的典型规避方式是只针对"频繁且长时间阻塞"的位置改用 ReentrantLock:
java
lock.lock();
try {
blockingIo();
} finally {
lock.unlock();
}
Java 24 的 JEP 491 改进了 HotSpot,使虚拟线程在持有 synchronized 监视器时也能在阻塞点卸载。到了 Java 26,常见剩余 pinning 场景主要是执行本地方法或 Foreign Function 时发生阻塞。
这并不代表 synchronized 从此没有死锁、锁竞争或临界区过大的问题。JEP 491 解决的是虚拟线程与 carrier 的固定问题,不会改变 Java 监视器本身的并发语义。
Java 26 还进一步优化了虚拟线程等待其他线程完成类初始化时的卸载行为,减少 carrier 被占住以及极端情况下调度停滞的风险。
4.7 观测虚拟线程
大量虚拟线程不适合只靠传统的操作系统线程视角观察。可以使用:
bash
jcmd <pid> Thread.dump_to_file -format=json threads.json
还应结合 JFR、应用指标、连接池指标、下游限流和请求超时一起判断。看到"虚拟线程数量很多"本身并不等于故障,关键要看它们阻塞在哪里、是否持续堆积,以及下游资源是否已经饱和。
5. 模式匹配详解:从 Java 21 的引用类型走向 Java 26 的基本类型
5.1 Java 21 正式支持记录模式
传统代码需要先判断类型,再强制转换,最后调用访问器:
java
if (obj instanceof Point) {
Point point = (Point) obj;
int x = point.x();
int y = point.y();
}
记录模式可以直接完成类型检查和数据解构:
java
record Point(int x, int y) {}
static int distanceSquared(Object obj) {
if (obj instanceof Point(int x, int y)) {
return x * x + y * y;
}
return 0;
}
记录模式还支持嵌套解构:
java
record Point(int x, int y) {}
record Line(Point start, Point end) {}
static int horizontalLength(Object obj) {
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))
&& y1 == y2) {
return Math.abs(x2 - x1);
}
return 0;
}
5.2 Java 21 正式支持 switch 模式匹配
配合 sealed 类型、record 和 switch,可以构建紧凑且可穷举检查的数据处理代码。
java
import java.math.BigDecimal;
sealed interface Payment permits CardPayment, CashPayment, CouponPayment {}
record CardPayment(String cardNo, BigDecimal amount) implements Payment {}
record CashPayment(BigDecimal amount) implements Payment {}
record CouponPayment(String code, BigDecimal amount) implements Payment {}
public class PaymentDemo {
static String describe(Payment payment) {
return switch (payment) {
case CardPayment(String cardNo, BigDecimal amount)
when amount.compareTo(new BigDecimal("10000")) > 0 ->
"大额银行卡支付,尾号:" + cardNo.substring(cardNo.length() - 4);
case CardPayment(String cardNo, BigDecimal amount) ->
"银行卡支付:" + amount;
case CashPayment(BigDecimal amount) ->
"现金支付:" + amount;
case CouponPayment(String code, BigDecimal amount) ->
"优惠券支付:" + code + ",金额:" + amount;
};
}
}
这里包含三种能力:
- 类型模式:判断对象属于哪个实现类型。
- 记录模式:匹配成功后直接提取 record 组件。
when守卫:在模式匹配成功后继续判断业务条件。
由于 Payment 是密封层次,编译器知道所有允许的子类型。当新增实现类时,未覆盖所有分支的 switch 会在编译期暴露问题。
5.3 null 处理更加明确
模式 switch 可以显式处理 null:
java
static String format(Object value) {
return switch (value) {
case null -> "<null>";
case String text when text.isBlank() -> "<blank>";
case String text -> text;
case Integer number -> "int:" + number;
default -> value.toString();
};
}
如果没有 case null,对 null 执行这类 switch 仍会抛出 NullPointerException。不要把模式匹配误解为自动的空安全。
5.4 Java 23---26:基本类型模式匹配持续预览
这条能力的演进路径是:
| 版本 | JEP | 状态 |
|---|---|---|
| Java 23 | JEP 455 | 第一次预览 |
| Java 24 | JEP 488 | 第二次预览 |
| Java 25 | JEP 507 | 第三次预览 |
| Java 26 | JEP 530 | 第四次预览 |
Java 26 中,可以用 instanceof 检查一次基本类型转换是否会丢失信息,并在成功时直接绑定变量:
java
public class PrimitivePatternDemo {
static void printAsByte(int value) {
if (value instanceof byte b) {
System.out.println("可以安全转换为 byte:" + b);
} else {
System.out.println("超出 byte 范围:" + value);
}
}
public static void main(String[] args) {
printAsByte(100);
printAsByte(1000);
}
}
编译和运行:
bash
javac --enable-preview --release 26 PrimitivePatternDemo.java
java --enable-preview PrimitivePatternDemo
Java 26 还允许 long、float、double、boolean 及其包装类型作为 switch 选择器:
java
static String rating(double value) {
return switch (value) {
case 0.0d -> "零分";
case double score when score > 0.0d && score < 3.0d -> "较低";
case double score when score >= 3.0d && score < 5.0d -> "良好";
case 5.0d -> "满分";
default -> "非法评分";
};
}
这仍是预览语法,不能因为已经连续预览四次就把它当作正式语言特性。
6. 结构化并发详解:让并发任务拥有清晰的生命周期
6.1 传统 Future 组合的问题
一个请求可能需要同时查询用户、订单和账户余额:
text
处理请求
├── 查询用户
├── 查询订单
└── 查询余额
传统线程池加 Future 可以并发执行,但开发者必须自己处理:
- 某个子任务失败后,其他任务是否取消。
- 父任务超时后,子任务是否仍在后台运行。
- 中断是否正确传播。
- 多个异常如何汇总。
- 线程转储中如何看出父子任务关系。
如果生命周期管理不严谨,很容易出现线程泄漏、无效远程调用和取消延迟。
6.2 结构化并发的核心约束
结构化并发借鉴结构化编程的思想:进入一个作用域,创建子任务,等待子任务结束,然后离开作用域。
text
进入作用域
├── fork 子任务 A
├── fork 子任务 B
├── join
└── 得到结果或异常
离开作用域:所有子任务均已结束
父任务不会在子任务仍然失控运行时悄悄结束。作用域把取消、失败传播、超时和可观测性放进同一个结构中。
6.3 Java 21 到 Java 26 的演进
| 版本 | JEP | 状态 | 关键说明 |
|---|---|---|---|
| Java 21 | JEP 453 | 第一次预览 | 从孵化器进入预览 API |
| Java 22 | JEP 462 | 第二次预览 | 延续反馈与验证 |
| Java 23 | JEP 480 | 第三次预览 | 延续预览 |
| Java 24 | JEP 499 | 第四次预览 | 延续预览 |
| Java 25 | JEP 505 | 第五次预览 | API 重新设计,引入 open、Joiner 与配置模型 |
| Java 26 | JEP 525 | 第六次预览 | 小幅调整命名、超时结果和配置函数类型 |
网上很多示例仍使用早期版本的 new StructuredTaskScope.ShutdownOnFailure()。这不是 Java 26 推荐的 API 形态,直接复制很可能无法编译。
6.4 Java 26 的基础示例
java
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class ProfileService {
record User(long id, String name) {}
record Order(long id, long userId) {}
record Profile(User user, List<Order> orders, int balance) {}
static Profile loadProfile(long userId) throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<User> userTask = scope.fork(() -> loadUser(userId));
Subtask<List<Order>> orderTask = scope.fork(() -> loadOrders(userId));
Subtask<Integer> balanceTask = scope.fork(() -> loadBalance(userId));
// 默认策略:全部成功则正常返回,任一失败则抛出 FailedException
scope.join();
return new Profile(
userTask.get(),
orderTask.get(),
balanceTask.get()
);
}
}
static User loadUser(long id) throws InterruptedException {
Thread.sleep(100);
return new User(id, "Alice");
}
static List<Order> loadOrders(long id) throws InterruptedException {
Thread.sleep(150);
return List.of(new Order(1L, id), new Order(2L, id));
}
static int loadBalance(long id) throws InterruptedException {
Thread.sleep(80);
return 1000;
}
public static void main(String[] args) throws InterruptedException {
System.out.println(loadProfile(1001L));
}
}
编译和运行:
bash
javac --enable-preview --release 26 ProfileService.java
java --enable-preview ProfileService
默认情况下,StructuredTaskScope 会为每个子任务创建虚拟线程。
6.5 Joiner:显式表达并发结果策略
如果只要第一个成功结果,可以使用 Java 26 的 anySuccessfulOrThrow():
java
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
static <T> T race(List<Callable<T>> tasks) throws InterruptedException {
try (var scope = StructuredTaskScope.open(Joiner.<T>anySuccessfulOrThrow())) {
tasks.forEach(scope::fork);
return scope.join();
}
}
第一个任务成功后,作用域会取消仍在运行且不再需要的任务。如果所有任务都失败,join() 抛出 StructuredTaskScope.FailedException。
Java 25 的同类方法名是 anySuccessfulResultOrThrow(),Java 26 改为 anySuccessfulOrThrow()。这正说明预览 API 不能依赖跨版本源代码兼容性。
6.6 超时控制
java
import java.time.Duration;
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
static List<String> loadAll() throws InterruptedException {
try (var scope = StructuredTaskScope.open(
Joiner.<String>allSuccessfulOrThrow(),
config -> config.withTimeout(Duration.ofSeconds(2)))) {
scope.fork(() -> query("service-a"));
scope.fork(() -> query("service-b"));
scope.fork(() -> query("service-c"));
return scope.join();
}
}
static String query(String service) throws InterruptedException {
Thread.sleep(100);
return service + ":ok";
}
如果超时在 join() 完成前到达,作用域会取消未完成任务,并根据 Joiner 策略抛出 StructuredTaskScope.TimeoutException 或返回自定义超时结果。
6.7 结构化并发与虚拟线程的关系
二者解决不同层次的问题:
- 虚拟线程解决"一个线程的资源成本过高"问题。
- 结构化并发解决"多个相关线程如何组成一个可靠任务"问题。
- Scoped Values 解决"上下文如何沿结构化调用链安全传递"问题。
三者组合后,Java 可以用更直接的同步代码表达高并发请求,同时保留清晰的父子关系、异常传播和线程转储信息。
7. Java 22:FFM API 正式定版,补齐底层互操作能力
7.1 Java 22 的 12 个 JEP
| 分类 | JEP 与特性 | 状态 |
|---|---|---|
| GC | JEP 423:Region Pinning for G1 | 正式 |
| 语言 | JEP 447:Statements before super(...) |
预览 |
| 本地互操作 | JEP 454:Foreign Function & Memory API | 正式 |
| 语言 | JEP 456:Unnamed Variables & Patterns | 正式 |
| 字节码 | JEP 457:Class-File API | 预览 |
| 启动器 | JEP 458:Launch Multi-File Source-Code Programs | 正式 |
| 语言/API | JEP 459:String Templates | 第二次预览,后续撤回 |
| 向量计算 | JEP 460:Vector API | 第七次孵化 |
| Stream | JEP 461:Stream Gatherers | 预览 |
| 并发 | JEP 462:Structured Concurrency | 第二次预览 |
| 入门语法 | JEP 463:Implicitly Declared Classes and Instance Main Methods | 第二次预览 |
| 并发上下文 | JEP 464:Scoped Values | 第二次预览 |
7.2 Foreign Function & Memory API
FFM API 让 Java 可以通过标准、受控的方式访问堆外内存和本地函数,逐步减少对 JNI 样板代码与 sun.misc.Unsafe 的依赖。
下面示例在受控 Arena 中分配堆外内存:
java
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
public class ForeignMemoryDemo {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(ValueLayout.JAVA_INT);
segment.set(ValueLayout.JAVA_INT, 0, 42);
int value = segment.get(ValueLayout.JAVA_INT, 0);
System.out.println(value);
}
}
}
Arena 关闭后,其中受管理的内存段失效,从生命周期层面降低悬空指针和忘记释放本地内存的风险。
7.3 未命名变量与模式
当变量必须出现在语法中、但业务上并不关心它的值时,可以使用 _:
java
try {
parse();
} catch (NumberFormatException _) {
System.out.println("格式错误");
}
它能明确告诉读者:这个值有意被忽略,而不是开发者忘记使用变量。
7.4 多文件源码直接启动
Java 22 增强源码启动模式。对于简单演示和脚本式程序,可以直接执行入口源码,并让启动器发现相关源码:
bash
java Main.java
它适合教学、原型和小工具,但复杂工程仍应使用 Maven、Gradle 等构建系统管理依赖、测试和制品。
8. Java 23:基本类型模式匹配首预览,ZGC 分代模式成为默认
8.1 Java 23 的 12 个 JEP
| 分类 | JEP 与特性 | 状态 |
|---|---|---|
| 语言 | JEP 455:Primitive Types in Patterns, instanceof, and switch |
预览 |
| 字节码 | JEP 466:Class-File API | 第二次预览 |
| 文档 | JEP 467:Markdown Documentation Comments | 正式 |
| 向量计算 | JEP 469:Vector API | 第八次孵化 |
| 底层 API | JEP 471:弃用 sun.misc.Unsafe 内存访问方法并准备移除 |
弃用 |
| Stream | JEP 473:Stream Gatherers | 第二次预览 |
| GC | JEP 474:ZGC 默认使用分代模式 | 正式行为变更 |
| 语言 | JEP 476:Module Import Declarations | 预览 |
| 入门语法 | JEP 477:Implicitly Declared Classes and Instance Main Methods | 第三次预览 |
| 并发 | JEP 480:Structured Concurrency | 第三次预览 |
| 并发上下文 | JEP 481:Scoped Values | 第三次预览 |
| 语言 | JEP 482:Flexible Constructor Bodies | 第二次预览 |
8.2 Markdown 文档注释
Java 23 支持使用 /// 编写 Markdown 风格的 Javadoc:
java
/// 根据用户 ID 查询用户。
///
/// - 找到用户时返回用户对象
/// - 用户不存在时返回空结果
///
/// @param id 用户 ID
/// @return 用户查询结果
User findById(long id) {
return repository.findById(id);
}
这不会让普通 .md 文件自动变成 Javadoc,而是为 Java 源码中的 API 文档提供另一种书写形式。
8.3 ZGC 的行为变化
Java 23 中,只要使用:
bash
java -XX:+UseZGC -jar app.jar
ZGC 就默认采用分代模式。原来的 -XX:+ZGenerational 不再是启用分代模式所必需的开关。升级时应清理已经冗余或即将失效的参数,并重新进行延迟、吞吐和堆占用压测。
8.4 String Templates 没有继续进入 Java 23
Java 21 和 Java 22 的 STR."...\{value}..." 属于预览语法。它没有成为 Java 23 特性,第三次预览提案也已撤回。
这件事很有代表性:预览不是"提前拿到的正式功能",而是一种获取实际反馈的发布机制。生产代码如果使用预览能力,就必须接受升级时改代码甚至移除代码的成本。
9. Java 24:AOT、Stream Gatherers、Class-File API 与虚拟线程集中增强
Java 24 一次交付了 24 个 JEP,是 Java 21---26 期间功能最密集的版本。
9.1 Java 24 的 JEP 全景
| 分类 | 主要特性 |
|---|---|
| GC | JEP 404 分代 Shenandoah(实验性)、JEP 475 G1 延迟屏障展开、JEP 490 移除非分代 ZGC、JEP 501 弃用 32 位 x86 端口 |
| 对象布局 | JEP 450 Compact Object Headers(实验性) |
| 本地调用与底层访问 | JEP 472 准备限制 JNI、JEP 498 使用 sun.misc.Unsafe 内存访问方法时告警 |
| 安全 | JEP 478 KDF API(预览)、JEP 496 ML-KEM、JEP 497 ML-DSA、JEP 486 永久禁用 Security Manager |
| AOT | JEP 483 Ahead-of-Time Class Loading & Linking |
| 标准 API | JEP 484 Class-File API、JEP 485 Stream Gatherers |
| 虚拟线程 | JEP 491 synchronized 场景不再固定虚拟线程 |
| 语言预览 | JEP 488 基本类型模式第二次预览、JEP 492 灵活构造器第三次预览、JEP 494 模块导入第二次预览、JEP 495 简单源码文件第四次预览 |
| 并发预览 | JEP 487 Scoped Values 第四次预览、JEP 499 结构化并发第四次预览 |
| 向量计算 | JEP 489 Vector API 第九次孵化 |
| 平台工具 | JEP 479 移除 Windows 32 位 x86 端口、JEP 493 无需 JMOD 链接运行时镜像 |
9.2 Stream Gatherers 正式定版
Gatherer 是 Stream 的自定义中间操作扩展点,可表达固定窗口、滑动窗口、扫描和折叠等操作。
java
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class GathererDemo {
public static void main(String[] args) {
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.toList();
System.out.println(windows);
// [[1, 2, 3], [4, 5, 6], [7]]
}
}
过去为了实现这类操作,常常需要先收集成列表、手写循环,或者引入第三方流式库。Gatherer 把它纳入标准 Stream 管线。
9.3 Class-File API 正式定版
java.lang.classfile 提供标准方式解析、生成和转换 .class 文件。字节码工具、代理框架、编译器、覆盖率工具不必只依赖内部 API 或第三方字节码模型。
它并不会自动替代 ASM、Byte Buddy 等成熟生态库,但为 Java 平台自身演进与工具开发提供了稳定、与 class 文件格式同步的标准基础。
9.4 安全边界进一步收紧
Java 24 永久禁用 Security Manager,并继续为限制 JNI 与淘汰 Unsafe 内存访问方法做准备。
升级旧系统时,应重点排查:
- 是否仍通过
System.setSecurityManager安装安全管理器。 - Agent、APM 或框架是否动态加载本地库。
- 是否直接调用
sun.misc.Unsafe的内存访问方法。 - 是否把
--add-opens、--add-exports当作永久解决方案。
这些变化体现了 Java 的"默认完整性"方向:平台会逐步减少任意修改内部状态、类结构和最终字段的通道。
10. Java 25:新的 LTS 基线,Scoped Values 与多项语言能力正式定版
10.1 Java 25 的 18 个 JEP
| 分类 | JEP 与特性 | 状态 |
|---|---|---|
| 安全 | JEP 470:PEM Encodings of Cryptographic Objects | 预览 |
| 常量 | JEP 502:Stable Values | 预览 |
| 平台 | JEP 503:移除 32 位 x86 端口 | 正式 |
| 并发 | JEP 505:Structured Concurrency | 第五次预览 |
| 并发上下文 | JEP 506:Scoped Values | 正式 |
| 模式匹配 | JEP 507:基本类型模式匹配 | 第三次预览 |
| 向量计算 | JEP 508:Vector API | 第十次孵化 |
| JFR | JEP 509:CPU-Time Profiling | 实验性 |
| 安全 | JEP 510:Key Derivation Function API | 正式 |
| 语言 | JEP 511:Module Import Declarations | 正式 |
| 语言 | JEP 512:Compact Source Files and Instance Main Methods | 正式 |
| 语言 | JEP 513:Flexible Constructor Bodies | 正式 |
| AOT | JEP 514:AOT Command-Line Ergonomics | 正式 |
| AOT | JEP 515:AOT Method Profiling | 正式 |
| JFR | JEP 518:Cooperative Sampling | 正式 |
| 对象布局 | JEP 519:Compact Object Headers | 正式产品特性 |
| JFR | JEP 520:Method Timing & Tracing | 正式 |
| GC | JEP 521:Generational Shenandoah | 正式产品特性 |
10.2 Scoped Values 正式定版
Scoped Value 适合沿调用链传递只读上下文,例如租户、Trace ID、认证主体和请求元数据。
java
public class ScopedValueDemo {
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
public static void main(String[] args) {
ScopedValue.where(TRACE_ID, "trace-20260827")
.run(ScopedValueDemo::handleRequest);
}
static void handleRequest() {
queryOrder();
}
static void queryOrder() {
System.out.println("traceId=" + TRACE_ID.get());
}
}
与 ThreadLocal 相比,Scoped Value 的绑定具有明确词法范围,值不可在作用域内被随意修改,也更适合虚拟线程与结构化并发的继承模型。
它不是所有 ThreadLocal 的直接替代品。如果业务确实需要线程内可变状态,就需要重新设计状态所有权,而不是强行把可变对象塞进 Scoped Value。
10.3 模块导入声明
Java 25 可以导入一个模块导出的所有包:
java
import module java.base;
public class ModuleImportDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
Map<String, Integer> scores = Map.of("Alice", 100, "Bob", 95);
System.out.println(names);
System.out.println(scores);
}
}
它主要降低教学、探索和模块化库使用时的大量导入噪声,不代表大型工程应该无条件取消精确导入。
10.4 紧凑源码文件与实例 main 方法
下面代码在 Java 25 已是正式语法:
java
void main() {
System.out.println("Hello, Java 25");
}
直接运行:
bash
java Hello.java
它降低初学者和小工具的入口门槛,但并没有废除传统的类声明与 public static void main(String[] args)。
10.5 灵活的构造器方法体
Java 25 允许在显式调用 super(...) 或 this(...) 之前执行不会访问当前未初始化对象的校验和计算。
java
class PositiveValue extends Number {
private final int value;
PositiveValue(int value) {
if (value <= 0) {
throw new IllegalArgumentException("value 必须大于 0");
}
super();
this.value = value;
}
@Override public int intValue() { return value; }
@Override public long longValue() { return value; }
@Override public float floatValue() { return value; }
@Override public double doubleValue() { return value; }
}
它解决了过去为了在 super(...) 前校验参数,不得不引入静态辅助方法或重复计算的问题。
11. Java 26:HTTP/3 正式加入标准客户端,AOT 与运行时继续增强
Java 26 已于 2026 年 3 月 17 日正式发布,GA 构建为 26+35。
11.1 Java 26 的 10 个 JEP
| JEP | 特性 | 状态 |
|---|---|---|
| 500 | Prepare to Make Final Mean Final | 正式告警与迁移机制 |
| 504 | Remove the Applet API | 正式移除 |
| 516 | Ahead-of-Time Object Caching with Any GC | 正式 |
| 517 | HTTP/3 for the HTTP Client API | 正式 |
| 522 | G1 GC: Improve Throughput by Reducing Synchronization | 正式 |
| 524 | PEM Encodings of Cryptographic Objects | 第二次预览 |
| 525 | Structured Concurrency | 第六次预览 |
| 526 | Lazy Constants | 第二次预览 |
| 529 | Vector API | 第十一次孵化 |
| 530 | Primitive Types in Patterns, instanceof, and switch |
第四次预览 |
11.2 Prepare to Make Final Mean Final
Java 26 开始对通过深反射修改 final 字段的行为发出警告,为未来默认限制这种能力做准备。
这不会立刻让所有旧框架失效,但序列化、ORM、依赖注入、Mock 和字节码增强框架都应该检查是否依赖深反射修改最终字段。
迁移的正确方向是:
- 优先升级相关框架。
- 使用构造器、工厂方法或受支持的反序列化扩展点。
- 只在明确模块范围内临时开放能力。
- 不要把全局放开 final 字段修改当作长期方案。
11.3 Applet API 被移除
Applet API 自 JDK 17 起就已经标记为待移除,Java 26 正式删除。现代浏览器早已不支持 Applet,普通服务端应用通常不会受到影响。
风险主要存在于:
- 仍保留 Applet 类型引用的历史源码。
- 反射扫描整个 JDK 类型集合的工具。
- 打包了年代久远桌面组件的内部系统。
11.4 G1 吞吐优化
JEP 522 通过减少应用线程与 GC 线程之间的同步,提高 G1 场景下的应用吞吐。它属于运行时内部优化,应用通常不需要改代码。
但"升级后自动更快"不能代替压测。GC 表现取决于堆大小、对象分配速率、存活集、CPU、NUMA、容器限制和暂停目标,仍应使用相同业务负载做升级前后对比。
11.5 Lazy Constants 第二次预览
Lazy Constant 表示最多初始化一次、之后不可修改的惰性值。JVM 可以像优化 final 字段一样理解它,同时允许初始化时机延后。
适合的概念场景包括:
- 首次使用时加载大型模型或字典。
- 延迟解析配置和元数据。
- 只在某条业务路径真正需要时初始化昂贵对象。
它仍是预览 API,不应在公共库的稳定 API 中直接暴露。
12. HTTP/3 详解:Java 26 标准客户端如何使用 QUIC
12.1 HTTP/3 与 HTTP/2 的根本区别
| 对比项 | HTTP/2 | HTTP/3 |
|---|---|---|
| 传输基础 | TCP | QUIC,运行在 UDP 之上 |
| 安全握手 | TCP 后再进行 TLS | QUIC 集成 TLS 1.3 |
| 多路复用 | 多个流共享一条 TCP 连接 | 多个 QUIC 流共享连接 |
| 队头阻塞 | TCP 丢包可能阻塞同连接上的其他流 | 单个流丢包通常不会阻塞其他流的数据交付 |
| 网络切换 | 连接通常与 IP/端口绑定 | QUIC 具备更好的连接迁移基础 |
HTTP/3 不保证每个请求都更快。它的收益通常在高延迟、丢包、移动网络和连接建立频繁的场景中更明显。
12.2 Java 26 使用 HTTP/3
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class Http3Demo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3)
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/"))
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("status=" + response.statusCode());
System.out.println("actualVersion=" + response.version());
}
}
HTTP/3 是 Java 26 的正式 API,不需要 --enable-preview。
12.3 HTTP/3 不是默认协议
Java 26 的 HttpClient 默认首选版本仍是 HTTP/2。只有显式设置客户端或请求的首选版本为 HTTP_3,客户端才会启用 HTTP/3 协商或发现逻辑。
即使首选 HTTP/3,最终响应也可能使用 HTTP/2 或 HTTP/1.1。因此必须检查:
java
response.version()
不要只看到请求配置了 HTTP_3,就认定线上已经走 QUIC。
12.4 请求级别指定 HTTP/3
Java 26 也可以只为某个请求指定首选版本:
java
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com/"))
.version(HttpClient.Version.HTTP_3)
.GET()
.build();
这适合渐进式验证,不必让同一个 HttpClient 的全部请求都优先尝试 HTTP/3。
12.5 控制 HTTP/3 发现模式
Java 26 新增 HttpOption.H3_DISCOVERY。JDK 内置客户端支持三种模式:
| 模式 | 行为 |
|---|---|
ANY |
由实现选择发现算法,默认模式,可能竞争建立 QUIC 和 TLS/TCP 连接 |
ALT_SVC |
先通过 HTTP/1.1 或 HTTP/2 获取服务器的 Alt-Svc 信息,再使用 HTTP/3 |
HTTP_3_URI_ONLY |
只直接尝试 URI 对应主机和端口的 HTTP/3,不回退 |
强制只尝试 HTTP/3:
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpOption;
import java.net.http.HttpRequest;
HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com/"))
.version(HttpClient.Version.HTTP_3)
.setOption(
HttpOption.H3_DISCOVERY,
HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY
)
.GET()
.build();
HTTP_3_URI_ONLY 更适合连通性测试和严格协议验证。普通业务如果需要兼容更多网络环境,通常应允许合理回退。
12.6 Java 26 HTTP/3 的边界
- 仅
https请求可能使用 HTTP/3。 - JDK 26 内置实现不通过代理使用 HTTP/3;有代理时通常降级,严格模式则失败。
- 服务端、负载均衡器、CDN、防火墙和网络路径都必须允许 QUIC/UDP。
java.net.http提供的是 HTTP/3 客户端能力,不是 HTTP/3 服务端实现,也不是通用 QUIC API。- 需要通过
response.version()、客户端日志、服务端日志和抓包共同验证实际协议。
生产验证时,可按网络类型统计 HTTP/3 成功率、回退率、握手耗时、请求延迟、丢包重传、UDP 被阻断比例,而不是只做一次本地请求。
13. AOT 详解:Java 24---26 如何改善启动与预热
13.1 Java 的 AOT 缓存解决什么问题
传统 HotSpot JVM 启动应用时,需要逐步完成:
text
读取 class
→ 解析 class
→ 加载与链接
→ 执行并收集方法画像
→ 识别热点
→ JIT 编译优化
长时间运行的服务通常能摊薄这些成本,但短生命周期任务、Serverless、命令行工具、弹性扩容实例和大型框架应用更关注冷启动与预热。
Project Leyden 的方向是把部分本来在运行时完成的工作提前,并把结果存入 AOT cache,在后续运行时复用。
13.2 Java 24:AOT Class Loading & Linking
JEP 483 在 CDS 基础上继续前进。CDS 主要提前读取和解析类,AOT cache 还可以让应用类以已加载、已链接的状态更快可用。
Java 24 的完整流程分三步。
第一步,使用代表性负载训练并记录配置:
bash
java -XX:AOTMode=record \
-XX:AOTConfiguration=app.aotconfig \
-cp app.jar com.example.Main
第二步,根据配置组装 AOT cache:
bash
java -XX:AOTMode=create \
-XX:AOTConfiguration=app.aotconfig \
-XX:AOTCache=app.aot \
-cp app.jar
第三步,生产运行时加载缓存:
bash
java -XX:AOTCache=app.aot \
-cp app.jar com.example.Main
Windows PowerShell 可以写成一行,或使用反引号进行续行;上面的反斜杠写法适用于 Bash。
13.3 Java 25:命令行简化与方法画像缓存
JEP 514 把常用的训练和组装合并成更简单的流程:
bash
java -XX:AOTCacheOutput=app.aot \
-cp app.jar com.example.Main
随后在生产阶段使用:
bash
java -XX:AOTCache=app.aot \
-cp app.jar com.example.Main
在 PowerShell 中:
powershell
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.Main
java -XX:AOTCache=app.aot -cp app.jar com.example.Main
JEP 515 还允许 AOT cache 保存训练运行中的方法执行画像。生产 JVM 启动后可以更快获得分支频率、类型分布等画像信息,从而减少重新收集画像的预热时间。
13.4 Java 26:AOT 对象缓存支持任意 GC
JEP 516 将缓存堆对象的格式与特定 GC 内部对象布局解耦。缓存对象可以从与 GC 无关的中立格式顺序加载,因此包括 ZGC 在内的垃圾收集器都能使用更完整的 AOT cache 能力。
这并不是"ZGC 从 Java 26 才能用 AOT",而是 Java 26 让包含缓存 Java 对象的高级 AOT 优化不再受特定 GC 对象布局限制。
13.5 AOT cache 不是 Native Image
| 对比项 | JDK AOT cache | Native Image 思路 |
|---|---|---|
| 输出 | .aot 缓存文件,仍配合 JVM 运行 |
本地可执行文件 |
| 运行时 | HotSpot 仍然存在,JIT 仍可继续优化 | 主要依赖构建期闭世界分析与本地代码 |
| 兼容性 | 保留传统 JVM 动态能力更多 | 反射、动态代理、资源等常需构建期配置 |
| 目标 | 改善启动和预热,同时保留 JVM 运行模型 | 极致冷启动和较小运行时形态 |
截至 Java 26,OpenJDK 主线已交付类加载与链接、方法画像、对象缓存等 AOT 优化,但 Project Leyden 页面仍把 AOT 代码编译列为后续工作。不能把 JDK AOT cache 描述成"Java 26 已把业务方法全部提前编译成本地机器码"。
13.6 AOT 使用注意事项
一个 AOT cache 与以下组合绑定:
- 具体应用及其 classpath、module path 或 JAR。
- 具体 JDK 版本。
- 操作系统与 CPU 架构。
- 训练和生产阶段需要保持一致的关键 JVM 选项。
只要应用依赖、JDK、操作系统镜像、CPU 架构或关键启动参数变化,就应该重新训练和生成缓存。
训练负载也必须具有代表性。只访问健康检查接口,无法覆盖真实业务会加载的类和热点路径。
生产建议使用默认的 AOTMode=auto 容错语义,并通过下面的日志确认缓存是否成功加载:
bash
java -XX:AOTMode=auto -Xlog:aot -XX:AOTCache=app.aot \
-cp app.jar com.example.Main
如果缓存不兼容,auto 模式可以继续在不使用该缓存的情况下启动;on 模式更适合作为严格验证和故障排查手段。
14. 其他重要演进主线
14.1 Scoped Values:从预览到正式
text
Java 21:第一次预览
Java 22:第二次预览
Java 23:第三次预览
Java 24:第四次预览
Java 25:正式
Java 26:正式延续
它与虚拟线程、结构化并发共同构成 Project Loom 在应用层的重要能力组合。
14.2 Stream Gatherers:两次预览后正式
text
Java 22:第一次预览
Java 23:第二次预览
Java 24:正式
如果项目最低版本仍是 Java 21,公共库不能直接暴露 Gatherer 类型;可以在 Java 24+ 的内部实现中使用,或通过多版本 JAR 管理差异。
14.3 Class-File API:跟上 class 格式演进
text
Java 22:第一次预览
Java 23:第二次预览
Java 24:正式
随着 class 文件格式每半年演进,平台自带 API 能降低工具链依赖内部实现的风险。
14.4 垃圾收集器的变化
- Java 21:分代 ZGC 正式交付。
- Java 23:分代 ZGC 成为默认模式。
- Java 24:移除非分代 ZGC,分代 Shenandoah 进入实验阶段。
- Java 25:Generational Shenandoah 成为产品特性。
- Java 26:G1 通过减少同步提高吞吐;AOT 对象缓存支持任意 GC。
GC 选择不能只看"新"或"低延迟"标签:
- G1 适合广泛的通用服务场景。
- ZGC 更关注超大堆与低停顿。
- Shenandoah 也面向低停顿,但可用性和支持情况取决于 JDK 发行版。
- Parallel GC 仍可能适合吞吐优先的批处理任务。
14.5 安全与完整性方向
这几个版本持续限制历史上的高风险通道:
- 动态加载 Agent 开始告警并为默认禁用做准备。
- Security Manager 被永久禁用。
- JNI 使用将逐步受到更明确的本地访问控制。
sun.misc.Unsafe内存访问方法被弃用并告警。- Java 26 开始对深反射修改
final字段发出警告。 - KEM、KDF、ML-KEM、ML-DSA、PEM 编解码能力逐步进入标准 API。
升级时不仅要编译通过,还要用真实启动参数检查运行时告警,因为很多完整性变化不会在普通源码编译阶段暴露。
15. 从 Java 21 升级到 Java 25 或 Java 26 的实践路线
15.1 第一步:先升级运行时,不急着改源码级别
如果项目当前使用 Java 21,可以先让应用在更高版本 JDK 上运行,同时保持:
xml
<maven.compiler.release>21</maven.compiler.release>
或者 Gradle:
groovy
java {
toolchain {
languageVersion = JavaLanguageVersion.of(26)
}
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
这样可以先暴露运行时兼容问题,再决定哪些模块需要采用新语法和新 API。
15.2 第二步:检查依赖和工具链
重点检查:
- Maven、Gradle 与插件是否识别目标 JDK。
- Spring Boot、Tomcat、Netty、Hibernate、Byte Buddy、ASM 是否支持对应 class 版本。
- APM、覆盖率、Mock、热更新和 Agent 是否依赖动态加载。
- JNI、本地库和
Unsafe调用是否触发告警。 - Docker 基础镜像和 CI 构建节点是否使用同一 JDK 发行版。
15.3 第三步:建立基准,而不是凭感觉判断新特性
虚拟线程至少比较:
- 固定吞吐下的线程数、内存与 CPU。
- 固定并发下的 P50、P95、P99 延迟。
- 数据库连接池、HTTP 连接池和下游限流等待。
- 超时、取消和异常传播。
AOT 至少比较:
- 冷启动时间。
- 首次请求延迟。
- 达到稳定吞吐所需时间。
- AOT cache 大小与镜像体积。
- 不同业务训练覆盖率。
HTTP/3 至少比较:
- 实际协商版本与回退比例。
- Wi-Fi、移动网络、高丢包网络下的延迟。
- UDP 被阻断时的失败和回退行为。
- CDN、网关、代理和服务端对 QUIC 的支持情况。
15.4 第四步:渐进采用新能力
推荐顺序:
- 先升级到受支持的新 JDK 并保持原有代码模型。
- 开启运行时日志,处理废弃与完整性告警。
- 对 I/O 密集接口灰度虚拟线程。
- 使用 Scoped Values 整理只读请求上下文。
- 对冷启动敏感应用验证 AOT cache。
- 有明确网络收益时再启用 HTTP/3。
- 结构化并发和基本类型模式匹配仍按预览特性隔离试点。
15.5 第五步:不要跳过回滚设计
- 虚拟线程保留平台线程执行器开关。
- HTTP/3 保留 HTTP/2 回退并记录实际版本。
- AOT cache 加载失败时允许无缓存启动。
- 预览特性集中封装,确保下个 JDK 版本可快速调整。
- GC 参数通过配置管理发布,不要写死在不可变脚本中。
16. 生产环境应该选择 Java 21、25 还是 26
16.1 选择 Java 21
适合:
- 需要成熟生态和较长验证历史。
- 正在从 Java 8、11 或 17 迁移。
- 主要目标是使用虚拟线程、记录模式和
switch模式匹配。 - 依赖的框架或中间件尚未完整验证 Java 25/26。
16.2 选择 Java 25
适合:
- 希望采用新的长期支持基线。
- 需要正式版 Scoped Values、模块导入、紧凑源码和灵活构造器。
- 关注 AOT 方法画像、紧凑对象头、JFR 增强与分代 Shenandoah。
- 能完成一次完整的框架、Agent 和运行时兼容验证。
16.3 选择 Java 26
适合:
- 明确需要标准 HTTP Client 的 HTTP/3 能力。
- 希望让高级 AOT 对象缓存与 ZGC 等任意 GC 组合。
- 愿意采用非 LTS 功能版并遵循更快升级节奏。
- 希望尽早验证结构化并发第六次预览或基本类型模式第四次预览。
保守的生产策略通常是"Java 21 或 Java 25 作为长期基线,Java 26 用于需要其独有能力的服务"。但真正决定因素应是发行版支持合同、框架兼容性、团队升级能力和实际压测结果。
17. 常见问题
17.1 虚拟线程能完全替代响应式编程吗
不能一概而论。
虚拟线程让同步阻塞代码拥有更好的并发扩展性,能显著降低很多请求型服务的代码复杂度。但事件流、背压、长连接推送和已有完整响应式数据管线仍有自己的价值。
不要为了"使用虚拟线程"把成熟、无阻塞的响应式系统全部重写;也不要在虚拟线程中继续堆叠没有必要的异步回调。
17.2 虚拟线程越多越好吗
虚拟线程本身很便宜,但任务依赖的数据库连接、Socket、堆内对象、请求缓冲区和下游容量并不无限。
应该取消"限制线程数量"的旧习惯,但保留"限制稀缺资源并发量"的工程纪律。
17.3 Java 26 的结构化并发可以直接作为公共库 API 吗
不建议。它仍是第六次预览,Java 25 到 Java 26 就发生了方法名和配置类型调整。
可以在应用内部试点,但最好通过自己的接口封装,不要让 StructuredTaskScope 类型扩散到公共 SDK。
17.4 Java 26 会默认使用 HTTP/3 吗
不会。默认首选仍是 HTTP/2,必须显式设置 HTTP_3。即使设置了,也可能根据服务端与网络条件回退,所以要检查响应的实际版本。
17.5 AOT cache 会让 JVM 不再 JIT 编译吗
不会。AOT cache 主要复用提前读取、解析、加载、链接的类,缓存对象和方法画像等资产。HotSpot 与 JIT 仍然存在,并会继续根据运行时情况优化代码。
17.6 Java 21 的 String Templates 能在 Java 26 中使用吗
不能把它当作现有正式语法。String Templates 只在 Java 21 和 22 中预览,后续提案已撤回,Java 23---26 没有该标准语言特性。
17.7 Java 24 以后 synchronized 就没有问题了吗
不是。Java 24 主要消除了虚拟线程因持有 Java 监视器而发生的常见 carrier pinning。锁竞争、死锁、临界区过大和错误的共享状态设计仍然存在,本地方法或 Foreign Function 阻塞也仍可能造成 pinning。
17.8 可以直接从 Java 21 升到 Java 26 吗
技术上可以跨版本升级,不必逐个版本部署。但验证必须覆盖中间版本累积的移除项、默认行为变化、JVM 参数、框架兼容性、Agent、本地库和序列化行为。
18. 总结
Java 21 到 Java 26 的演进,可以概括为五个方向:
- 并发模型更简单。 虚拟线程让同步代码重新具备高吞吐潜力,结构化并发和 Scoped Values 则补齐任务生命周期与上下文传递。
- 语言更擅长表达数据。 record、sealed class、记录模式和
switch模式匹配组合后,领域模型处理更紧凑、更容易被编译器检查。 - JVM 更关注冷启动和预热。 AOT cache 从类加载链接扩展到方法画像与跨 GC 对象缓存,同时保留 HotSpot 和 JIT 的动态优化能力。
- 标准库覆盖更现代的基础设施。 FFM、Stream Gatherers、Class-File API、KDF、后量子密码算法与 HTTP/3 陆续进入平台。
- 平台边界更严格。 Security Manager、32 位端口、Unsafe 内存访问、JNI、动态 Agent 和 final 字段深反射都在被清理或收紧。
如果只选最值得立即掌握的内容:
- Java 21:虚拟线程、记录模式、
switch模式匹配。 - Java 22:FFM API、未命名变量。
- Java 23:了解基本类型模式匹配与预览机制的边界。
- Java 24:Stream Gatherers、Class-File API、虚拟线程去 pinning、AOT 起点。
- Java 25:Scoped Values、正式语言改进、AOT 方法画像。
- Java 26:HTTP/3、任意 GC 的 AOT 对象缓存,以及仍处于预览阶段的结构化并发。
真正稳妥的升级方式,不是看到新语法就全面重写,而是先升级运行时、建立可重复基准、处理兼容告警,再用灰度和回滚机制逐步采用能解决实际问题的新能力。
19. 官方资料
19.1 各版本特性列表
- OpenJDK JDK 21
- OpenJDK JDK 22
- OpenJDK JDK 23
- OpenJDK JDK 24
- OpenJDK JDK 25
- Oracle JDK 26 Release Notes
19.2 核心 JEP
- JEP 444:Virtual Threads
- JEP 440:Record Patterns
- JEP 441:Pattern Matching for switch
- JEP 491:Synchronize Virtual Threads without Pinning
- JEP 525:Structured Concurrency (Sixth Preview)
- JEP 530:Primitive Types in Patterns, instanceof, and switch (Fourth Preview)
- JEP 517:HTTP/3 for the HTTP Client API
- JEP 483:Ahead-of-Time Class Loading & Linking
- JEP 514:Ahead-of-Time Command-Line Ergonomics
- JEP 515:Ahead-of-Time Method Profiling
- JEP 516:Ahead-of-Time Object Caching with Any GC
- Project Leyden