Java 26 深度实战:Lazy Constants 干掉双重检查锁、HTTP/3 客户端开箱即用、原始类型模式匹配杀疯了——10 个核心 JEP 一次讲透
2026 年 3 月 17 日,JDK 26 正式 GA。作为 Java 六个月发布节奏下的又一个特性版(feature release),它没有像 JDK 25 那样戴上 LTS 的帽子,却在语言层面、标准库层面和 JVM 层面一口气交付了 10 个 JEP。本文不堆砌 Release Notes,而是以程序员视角,把其中真正能改变你日常写法的几个 JEP 拆开揉碎:Lazy Constants 怎么让"延迟初始化的 final"成为现实、HTTP/3 客户端为什么一行就能切、原始类型模式匹配凭什么终结 switch 的"只认引用类型"时代、结构化并发第六次预览又收敛了什么,以及 AOT 对象缓存终于能和 ZGC 同框。每个点都配可运行代码,并在最后给出一份面向生产的升级 checklist。
背景介绍:为什么说 JDK 26 是"被低估的特性版"
先对齐几个事实,避免被版本号绕晕:
- Java 从 JDK 9 起固定六个月一个特性版(3 月 / 9 月),长期支持版(LTS)大约每两年一个。JDK 21(2023-09)和 JDK 25(2025-09)是最近的 LTS,下一个 LTS 是 JDK 29(2027-09)。
- JDK 26 于 2026-03-17 GA,是 JDK 25 之后的非 LTS 特性版。它承接了一批在 JDK 23/24/25 反复预览、打磨的特性(模式匹配、结构化并发、Lazy Constants 前身 StableValue、PEM 等),把它们继续向前推。
- 本次全部 10 个 JEP 一览:
| JEP | 标题 | 类别 | 状态 |
|---|---|---|---|
| 500 | Prepare to Make Final Mean Final | 语言(准备性) | 交付 |
| 504 | Remove the Applet API | 类库清理 | 交付 |
| 516 | Ahead-of-Time Object Caching with Any GC | JVM / GC | 交付 |
| 517 | HTTP/3 for the HTTP Client API | 类库(网络) | 交付 |
| 522 | G1 GC: Improve Throughput by Reducing Synchronization | JVM / GC | 交付 |
| 524 | PEM Encodings of Cryptographic Objects(Second Preview) | 安全类库 | 第二次预览 |
| 525 | Structured Concurrency(Sixth Preview) | 并发类库 | 第六次预览 |
| 526 | Lazy Constants(Second Preview) | 语言/类库 | 第二次预览 |
| 529 | Vector API(Eleventh Incubator) | 类库( incubating) | 第十一次孵化 |
| 530 | Primitive Types in Patterns, instanceof, and switch(Fourth Preview) | 语言 | 第四次预览 |
可以看到一个明显趋势:Amber 项目(语言精致化)和 Leyden 项目(启动/预热优化)是两条主线。模式匹配、Lazy Constants 属于让代码更短更安全的 Amber 路线;AOT 对象缓存、Vector API 属于让 Java 跑得更快更稳的 Leyden/Panama 路线。下面按"值不值得你立刻上手"的优先级逐个拆解。
阅读提示:带"Preview / Incubator"的 JEP 需要显式开启。预览语言特性编译/运行加
--enable-preview --source 26;孵化模块(Vector API)运行加--add-modules jdk.incubator.vector。生产环境请务必评估转正节奏(见文末 checklist)。
一、Lazy Constants(JEP 526):终于有"延迟初始化的 final"了
核心概念:final 的两难
《Effective Java》第三条就劝你"优先使用不可变性"。Java 手里最趁手的不可变工具是 final 字段——但 final 有个死规矩:必须在构造期(实例字段)或类初始化期(静态字段)一次性赋值,而且初始化顺序被源码书写顺序绑定。
这带来两个真实痛点:
- 启动拖累:一个类加载时就要把所有可能用到的重量级对象(配置、连接池、缓存)全部
final初始化好,哪怕这次请求根本用不到。 - 时间耦合:A 依赖 B,B 依赖 A,你不得不上"惰性初始化 + 双重检查锁(double-checked locking)"那套样板代码,既丑又容易写错(忘了
volatile就埋雷)。
JEP 526 给出的解法是 LazyConstant:一个持有不可变数据的对象,但初始化时机由你决定,同时 JVM 把它当成真正的常量来优化(享受和 final 同等的常量折叠、逃逸分析红利)。
架构分析:为什么它比"自己写懒加载"强
Lazy Constants 的底层机制与 JVM 内部的 @Stable 注解同源——HotSpot 早就在用类似的"稳定值"优化内部代码。JEP 526 把它以受控 API 暴露出来,并保证:
- 至多初始化一次,多线程下也安全(内部已处理可见性与竞态,你不用自己加锁);
- 计算结果为 null 会被拒绝(与不可变集合、ScopedValue 的语义一致,顺便换来性能);
- 相比 JDK 25 的
StableValue(JEP 502),JDK 26 把 API 重命名为LazyConstant,砍掉orElseSet/setOrThrow/trySet等偏底层的写入方法,只保留"吃一个计算函数"的工厂方法,并把集合的懒加载工厂挪进了java.util.List/java.util.Map接口,整体更聚焦"高频用例"。
代码实战
最经典的用法——替代双重检查锁的单例与配置加载:
import java.lang.LazyConstant;
public final class AppConfig {
// 类加载时不再立即读盘;首次 get() 才计算,之后恒定
private static final LazyConstant<AppConfig> INSTANCE =
LazyConstant.of(AppConfig::loadFromDisk);
private final String region;
private final int maxConnections;
private AppConfig(String region, int maxConnections) {
this.region = region;
this.maxConnections = maxConnections;
}
private static AppConfig loadFromDisk() {
// 假设这里有真实 IO / 解析逻辑
System.out.println("loading config (once)...");
return new AppConfig("ap-east-1", 64);
}
public static AppConfig instance() {
return INSTANCE.get(); // 线程安全、只算一次
}
public String region() { return region; }
public int maxConnections() { return maxConnections; }
}
把上面的 AppConfig 和"经典双重检查锁单例"对比,你会发现 Lazy Constants 版本:
- 没有
volatile字段; - 没有
if (instance == null)的两次判空; - 没有"忘记加锁导致重复构造"的隐患;
- 语义上等价于
private static final AppConfig INSTANCE = ...,但初始化延后到了真正被需要的时候。
集合场景也顺手。下面这个例子用 LazyConstant 组合出一个"按需计算、但只算一次"的列表(JDK 26 也在 List / Map 接口上提供了对应的懒加载便捷工厂,思路一致):
import java.lang.LazyConstant;
import java.util.List;
import java.util.stream.IntStream;
public class ExpensiveTable {
private static final int N = 1_000;
// 每个元素都是延迟常量:用到才计算,且只算一次
private static final List<LazyConstant<Double>> VALUES =
IntStream.range(0, N)
.mapToObj(i -> LazyConstant.of(() -> heavyCompute(i)))
.toList();
private static double heavyCompute(int i) {
// 昂贵计算,例如查表 / 数值积分
return Math.pow(i, 2) * Math.PI;
}
public static double get(int i) {
return VALUES.get(i).get();
}
}
性能优化视角
Lazy Constants 的核心收益是把"启动期的一次性重活"摊薄到"运行期的按需计算",直接砍掉冷启动尾延迟。更妙的是,由于 JVM 把 LazyConstant 当 final 处理,热点路径上 INSTANCE.get() 的值能被常量折叠/内联,几乎零开销。对于 Serverless、CLI、短生命周期容器这类"启动即峰值"的场景,这是免费的性能午餐。
二、原始类型模式匹配(JEP 530):switch 终于吃所有基本类型
核心概念
模式匹配(pattern matching)从 JDK 14 的 instanceof、JDK 16 的 record 模式一路走到今天,但一直有个尴尬限制:switch 的表达式/语句只认引用类型的类型模式(如 case Integer i、case String s、case Point(int x, int y)),原始类型(int/long/double…)只能老老实实写 case 0: case 1: 这种"裸常量",匹配到的值还拿不到。
JEP 530(第四次预览)把这个口子彻底撕开:switch 现在可以匹配任意原始类型,并且能把匹配到的值绑定到一个同类型变量上。
代码实战
把"未知状态"从无信息量的 default 变成有数据的 case int i:
// 旧写法:default 里拿不到原始状态值,只能再调一次方法
String oldDescribe(int status) {
switch (status) {
case 0 -> { return "ok"; }
case 1 -> { return "warning"; }
case 2 -> { return "error"; }
default -> { return "unknown: " + status; }
}
}
// JEP 530:default 升级为原始类型模式,直接暴露值
String describe(int status) {
return switch (status) {
case 0 -> "ok";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i; // i 就是匹配到的 int
};
}
配合 when 守卫做区间判断,再也不用 if-else 收尾:
String membershipTier(int yearlyFlights) {
return switch (yearlyFlights) {
case 0 -> "none";
case 1, 2 -> "silver";
case int i when i >= 100 -> "gold";
case int i -> "regular(" + i + ")";
};
}
instanceof 也能直接解原始类型(以前 instanceof 对原始类型毫无意义,因为 (int) 不能当引用判型;现在的语义是"安全转换式的解构"):
void handle(Object payload) {
if (payload instanceof int i) {
System.out.println("got primitive int: " + i);
} else if (payload instanceof String s) {
System.out.println("got string: " + s);
}
}
Record 模式对原始组件也更友好了——以前组件是原始类型时,record 模式必须精确写死类型,不一致就报错;现在自动转换更贴合语言其他部分的习惯:
record Point(int x, int y) {}
void print(Object o) {
if (o instanceof Point(int x, int y)) { // 原始组件直接解构
System.out.println("point at " + x + "," + y);
}
}
架构分析:dominance 检查更严了
JEP 530 第四次预览相比前几次有两点实质变化:unconditional exactness 的定义被细化,以及 switch 的 dominance(支配)检查更严格——编译器现在能帮你揪出更多"写了也不可能命中"的分支。代价是:少数以前能编过的 switch 构造现在会被拒。升级时若遇到相关编译错误,基本是把过于宽泛的 case 顺序调整一下即可,属于"编译器替你排雷"。
三、HTTP/3 客户端(JEP 517):一行 opt-in,QUIC 红利到手
背景:为什么是 HTTP/3
HTTP/2 解决了应用层的队头阻塞(head-of-line blocking),但它跑在 TCP 上——一旦某个 TCP 包丢了,同连接上所有流都得等它重传,这就是传输层的队头阻塞。HTTP/3 把传输层换成 QUIC(基于 UDP,内置 TLS 1.3),多路复用独立成流,丢包只影响那条流;握手更快、弱网更稳。截至 JDK 26 发布,约三分之一的网站已部署 HTTP/3。
JDK 11 的 java.net.http.HttpClient 只支持 HTTP/1.1 和 HTTP/2。JEP 517 把 HTTP/3 也加上了。
代码实战:注意是 opt-in
关键点——默认协议版本不会从 HTTP/2 切到 HTTP/3,你需要显式开启。这是有意为之:避免默认行为突变导致意外。
import java.net.http.*;
import java.net.URI;
public class H3Client {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_3) // 显式 opt-in
.build();
HttpRequest req = HttpRequest.newBuilder(URI.create("https://example.com"))
.GET()
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("status=" + res.statusCode() + ", version=" + res.version());
}
}
几个落地细节:
- 如果目标服务器不支持 HTTP/3,客户端会像以前一样透明降级到 HTTP/2(再不行到 HTTP/1.1),代码无需改动。你可以
res.version()确认实际协商到的版本。 - JEP 517 只做客户端,不提供服务端实现,也不暴露底层 QUIC API,也不动老
java.net.URL(它仍只有 HTTP/1.1)。 - 想强制只走 HTTP/3、不允许降级,可以用
HttpClient.newBuilder().version(HTTP_3)配合更细的version策略(如HTTP_3单独设置而不带 HTTP_2 兜底),但要权衡兼容性。
什么时候值得上
移动端、跨洲链路、高丢包弱网、以及大量短连接场景,HTTP/3 的收益最明显(握手快、无传输层队头阻塞)。机房内低延迟稳定网络里,HTTP/2 已经够好,不必强行切。
四、结构化并发(JEP 525):第六次预览,scope 进一步收敛
核心概念
并发编程最常见的反模式是"把一堆无关任务丢进线程池,再用 Future.get() 把异常吞掉、取消信号传不回去"。Loom 项目的结构化并发用一个 StructuredTaskScope 把这些"同属一个任务"的子任务绑成一个单元:要么一起成功,要么一个失败就取消其余,错误传播和取消语义都干净。
本次预览的变化
JDK 25 把 StructuredTaskScope 的公有构造器换成了静态工厂 open(...);JDK 26(第六次预览,JEP 525)进一步收敛:
- 新增
Joiner.onTimeout(Duration, Supplier),超时可直接返回一个兜底结果; Joiner.allSuccessfulOrThrow()现在返回List<结果>而不是Stream<Subtask>(用起来更直观);Joiner.anySuccessfulResultOrThrow()改名为anySuccessfulOrThrow();open(...)接收Joiner+ 一个UnaryOperator配置函数。
代码实战:并行取数 + 超时兜底
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
import java.time.Duration;
public class ProfileService {
record Profile(User user, java.util.List<Order> orders) {}
public Profile load(String userId) throws Exception {
// allSuccessfulOrThrow:任一子任务失败则整体抛异常
try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
Subtask<User> user = scope.fork(() -> fetchUser(userId));
Subtask<java.util.List<Order>> orders = scope.fork(() -> fetchOrders(userId));
scope.join(); // 等所有子任务结束
return new Profile(user.get(), orders.get());
} // 离开 try-with-resources 时,未完成的子任务会被取消
}
// 带超时兜底:800ms 内没全部完成,返回降级结果而非抛异常
public Profile loadWithTimeout(String userId) throws Exception {
try (var scope = StructuredTaskScope.open(
Joiner.allSuccessfulOrThrow()
.onTimeout(Duration.ofMillis(800), () -> FALLBACK))) {
Subtask<User> user = scope.fork(() -> fetchUser(userId));
Subtask<java.util.List<Order>> orders = scope.fork(() -> fetchOrders(userId));
scope.join();
return new Profile(user.get(), orders.get());
}
}
private static final Profile FALLBACK = new Profile(User.GUEST, java.util.List.of());
// 下面是实现占位
private User fetchUser(String id) { return User.GUEST; }
private java.util.List<Order> fetchOrders(String id) { return java.util.List.of(); }
}
结构化并发的精髓在最后一个右花括号:scope 关闭即取消所有还在跑的子任务,不会有"父任务结束了、子线程还在偷偷跑"的孤儿线程。配合虚拟线程(Loom 已在 JDK 21 转正),这是 Java 写高并发 IO 的最清爽范式。
五、PEM 编解码(JEP 524):终于不用引 Bouncy Castle 了
背景
密钥、证书、CRL 在网络上传输几乎都用 PEM(Privacy-Enhanced Mail)文本格式——就是那一堆 -----BEGIN PRIVATE KEY----- 包裹的 Base64。可 Java 标准库一直没有一个干净的 API 在 "PEM 文本 ↔ 密钥/证书对象" 之间互转,大家要么手写 Base64 + 解析,要么拉 Bouncy Castle。JEP 470(JDK 25)首次预览,JEP 524(JDK 26)第二次预览,把这套 API 打磨得更顺手。
本次变化
PEMRecord改名为PEM,并新增decode()方法返回解码后的 DER 内容;EncryptedPrivateKeyInfo的加密方法改名为encrypt,改为接受DEREncodable(可加密KeyPair、PKCS8EncodedKeySpec);PEMEncoder/PEMDecoder现在支持KeyPair和PKCS8EncodedKeySpec的加解密。
代码实战
import java.security.PEM;
import java.security.PEMDecoder;
import java.security.PEMEncoder;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
public class PemDemo {
public static void main(String[] args) throws Exception {
String privateKeyPem = """
-----BEGIN PRIVATE KEY-----
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA...
-----END PRIVATE KEY-----
""";
// PEM 文本 -> 密钥对象
PrivateKey key = PEMDecoder.of().decodePrivateKey(privateKeyPem);
// 证书同理
// X509Certificate cert = PEMDecoder.of().decodeCertificate(certPem);
// 对象 -> PEM 文本
String out = PEMEncoder.of().encode(key);
System.out.println(out);
// 想拿回原始 DER 字节
PEM pem = PEMDecoder.of().decode(privateKeyPem);
byte[] der = pem.decode();
}
}
注意 JEP 524 仍是第二次预览,生产使用前请确认你的 JDK 26 构建开启了预览,并评估它何时转正(PEM 这类"标准格式互转"需求极强,转正概率高)。
六、AOT 对象缓存支持任意 GC(JEP 516):Leyden 终于打通 ZGC
背景:两类尾延迟,只能二选一?
HotSpot 的 GC 大多会暂停应用线程来回收内存,于是即便 99% 请求 10ms 内返回,1% 可能飙到 100ms+(尾延迟)。ZGC 用并发回收把暂停控制在 1ms 以内,是压尾延迟的利器。
但另一类尾延迟来自冷启动:Java 常用"拉新 JVM 实例"来横向扩容,而新实例要重新加载/链接上万类、重新预热 JIT,前几个请求明显更慢。Project Leyden 的 AOT 缓存(JDK 24 的 AOT 类加载/链接、JDK 25 的 AOT 方法画像)正是为此而生——训练运行把类缓存下来,生产直接"类已就位"。Spring PetClinic 实测启动快了 41%。
矛盾来了:旧版 AOT 缓存的对象格式和具体 GC 绑定,跟 ZGC 不兼容。于是你只能在"用 ZGC 压 GC 尾延迟"和"用 AOT 缓存压启动尾延迟"之间二选一。
JEP 516 的解法
JEP 516 让 AOT 缓存以GC 中立的序列化格式存储,加载时再顺序实例化到内存,从而支持任意 GC,包括 ZGC。两类尾延迟优化现在能同时吃。
代码/命令实战
# 1) 训练运行:跑一遍典型路径,记录类与对象到缓存文件
java -XX:AOTCacheOutput=app.aotcache -cp app.jar com.example.Main
# 2) 生产运行:复用缓存,并同时开启 ZGC
java -XX:+UseZGC -XX:AOTCache=app.aotcache -cp app.jar com.example.Main
这是运维层优化,不改业务代码。对 Serverless / 容器扩缩容 / 微服务冷启动敏感的场景,组合拳收益显著:启动更快 + 暂停更短。
七、其余几个值得记账的 JEP
JEP 522 — G1 GC 通过减少同步提升吞吐:纯 JVM 内部优化,无 API 变化。G1 仍是默认 GC,这次主要在内部降低锁竞争,高并发分配压力下吞吐更好。升级即白拿,无需改代码。
JEP 529 — Vector API(第十一次孵化):用
jdk.incubator.vector把 SIMD 指令暴露给 Java,写数值计算(矩阵、编解码、哈希)能直接吃满 CPU 向量单元。仍是孵化态,需要--add-modules jdk.incubator.vector:import jdk.incubator.vector.*; static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED; void saxpy(int[] y, int[] x, int a) { for (int i = 0; i < SPECIES.loopBound(y.length); i += SPECIES.length()) { IntVector vx = IntVector.fromArray(SPECIES, x, i); IntVector vy = IntVector.fromArray(SPECIES, y, i); vy.mul(a).add(vx).intoArray(y, i); // 批量计算 } }JEP 500 — Prepare to Make Final Mean Final:为 Valhalla 项目铺路的"准备性" JEP,让 class 文件里的
final在 JVM 层面真正不可被绕过地生效。对应用层基本无感知,属于"未来价值"的一环。JEP 504 — Remove the Applet API:
java.applet包正式移除(之前已弃用多年)。还在维护古董Applet代码的,该迁移到javax.swing/ JavaFX / 纯 Web 了。
性能优化小结(落到生产)
把本文能直接带来性能收益的点收一下口:
- 冷启动:Lazy Constants(按需初始化,JVM 当 final 优化)+ AOT 缓存(JDK 24/25 已铺垫,JDK 26 打通任意 GC)+ ZGC = 启动尾延迟与 GC 尾延迟双压。容器/Serverless 优先评估。
- 并发 IO:结构化并发 + 虚拟线程,是 Java 写"并行聚合多个下游"最干净、最不易漏取消的范式。
- CPU 密集计算:Vector API(孵化)适合编解码、数值内核;G1 吞吐提升属于无感白拿。
- 网络:跨弱网/移动端链路,HTTP/3 客户端 opt-in 一行切换,自动降级保兼容。
总结与展望:哪些该上,哪些再等等
JDK 26 的 10 个 JEP 勾勒出两条清晰主线:Amber 让代码更短更安全(模式匹配、Lazy Constants),Leyden/Panama 让 Java 更快更稳(AOT 缓存、Vector API)。对一线开发的建议:
- 立刻能用(标准特性,无需预览):HTTP/3 客户端(JEP 517)、G1 吞吐(JEP 522)、AOT 缓存 + 任意 GC(JEP 516,运维层)、移除 Applet(JEP 504,清理)。
- 强烈建议试,但盯紧转正节奏(预览/孵化):Lazy Constants(JEP 526,第二次预览)、原始类型模式匹配(JEP 530,第四次预览)、结构化并发(JEP 525,第六次预览)、PEM(JEP 524,第二次预览)、Vector API(JEP 529,第十一次孵化)。这些在 JDK 26 里已经相当成熟,但 API 仍可能微调——内部工具、新项目可放心吃,对外强约束的库谨慎。
- 升级 checklist:① 用
jdeprscan/japicmp扫一遍依赖对 Applet 等移除项的依赖;② 预览特性统一在构建脚本加--enable-preview --source 26;③ AOT 缓存按"训练运行 → 生产加载"两段式接入 CI;④ 在弱网环境灰度验证 HTTP/3 协商与降级;⑤ 用jmh对核心路径做升级前后基准对比,别凭感觉判断快慢。
Java 没有停下。六个月后 JDK 27 又会带来新一轮预览与转正——而 JDK 26,正是把过去两年 Amber / Leyden 的成果"临门一脚"钉进标准的地方。现在上手,刚好卡在红利释放的窗口期。
参考(官方 JEP)
- JDK 26 总览:https://openjdk.org/projects/jdk/26/
- JEP 526 Lazy Constants:https://openjdk.org/jeps/526
- JEP 530 Primitive Types in Patterns:https://openjdk.org/jeps/530
- JEP 517 HTTP/3 for HTTP Client:https://openjdk.org/jeps/517
- JEP 525 Structured Concurrency:https://openjdk.org/jeps/525
- JEP 524 PEM Encodings:https://openjdk.org/jeps/524
- JEP 516 AOT Object Caching with Any GC:https://openjdk.org/jeps/516