

从 JDK 8 到 JDK 25:一份迟到但值得的升级指南
2025 年 9 月 16 日,JDK 25 正式 GA,成为继 JDK 21 之后的新一代 LTS 版本,Oracle 承诺支持至少到 2033 年。如果你还在 JDK 8 或 JDK 11 的世界里安稳度日——别急,这篇文章不是来批判你的。
2025 年 9 月 16 日,JDK 25 正式 GA,成为继 JDK 21 之后的新一代 LTS 版本,Oracle 承诺支持至少到 2033 年。如果你还在 JDK 8 或 JDK 11 的世界里安稳度日——别急,这篇文章不是来批判你的。事实上,JDK 8 服役十年仍然坚挺,说明它确实够用。但 JDK 25 带来的东西,值得你认真看一看。
这篇文章不打算把 18 个 JEP 像流水账一样过一遍。我会把真正影响你日常编码的特性拎出来讲透,附上可运行的代码;那些偏底层或仍在预览的特性,点到为止。
一、语言层面:写更少的代码,表达更多的意图#
1.1 紧凑源文件与实例 main 方法(JEP 512)⭐#
还记得你第一次写 Java 时的仪式感吗?
// JDK 8 的 Hello World —— 每一行都散发着"企业级"的气息
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}javaJDK 25 终于对初学者和脚本场景释放了善意:
// JDK 25 —— 没有 class,没有 static,没有 String[] args
void main() {
System.out.println("Hello, World!");
}java不需要声明类,不需要 static,不需要 String[] args。文件本身就是一个隐式的类,main 是它的实例方法。
这不是玩具特性。如果你写过 shell 脚本调 Java 来做数据处理或快速验证逻辑,这个特性能让你的 .java 文件像 Python 脚本一样简洁。当然,正式项目的入口类该怎么写还是怎么写——这是降低门槛,不是降低标准。
1.2 灵活的构造函数体(JEP 513)⭐#
这是一个等了二十多年的修正。在 JDK 8 中,super() 必须是构造函数的第一条语句,没有商量的余地:
// JDK 8 —— 参数校验只能放在 super() 之后,或者用丑陋的静态方法
class Employee extends Person {
Employee(String name, int age) {
super(age); // 必须第一行,哪怕 age 是 -1 也得先调
if (age < 18 || age > 67) {
throw new IllegalArgumentException("年龄不合法");
}
this.name = name;
}
}javaJDK 25 允许在 super() 之前执行语句,前提是这些语句不访问当前实例(this):
// JDK 25 —— 先校验,再调 super(),干净利落
class Employee extends Person {
final String name;
Employee(String name, int age) {
if (age < 18 || age > 67) {
throw new IllegalArgumentException("年龄必须在 18-67 之间");
}
super(age); // 校验通过了再构造父类
this.name = name;
}
}java这意味着 fail-fast 真正落到了构造函数的最前面。不再需要 validate(age) 这样的静态辅助方法,不再有“先创建了一半的非法对象”。逻辑更清晰,代码更安全。
1.3 模块导入声明(JEP 511)#
JDK 8 中我们习惯了一堆 import 语句:
import java.util.*;
import java.util.stream.*;
import java.time.*;javaJDK 25 引入了模块级导入:
import module java.base;java这一行等价于导入 java.base 模块导出的所有包。对于日常开发来说,这减少了大量的 import 语句。但要注意歧义问题——如果同时导入 java.base 和 java.sql 模块,Date 这个名字会产生冲突,需要单独用 import java.sql.Date; 来消歧。
这个特性对现有项目影响不大(IDE 会帮你管理 import),但在写脚本、教学或快速原型时很方便。
二、并发模型:从线程池到虚拟线程的范式转移#
如果你从 JDK 8 直接跳到 JDK 25,并发编程领域的变化堪称天翻地覆。这是 JDK 8 到 JDK 25 之间最值得深入理解的主题。
2.1 虚拟线程(JDK 21 转正,JDK 25 继续完善)⭐⭐#
这是 Project Loom 的核心交付物。在 JDK 8 中,每个线程都是一个操作系统线程,创建和切换的开销决定了你必须使用线程池:
// JDK 8 —— 经典的线程池模式
ExecutorService pool = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10000; i++) {
pool.submit(() -> {
// 处理请求,线程池大小就是并发天花板
handleRequest();
});
}java200 个线程已经不少了。如果每个请求要等 IO 100ms,你的吞吐量就被死死地卡在 2000 QPS。想提高?加线程——但操作系统线程的内存开销(每个约 1MB 栈空间)会让你的服务器先 OOM。
虚拟线程彻底改变了这个公式:
// JDK 25 —— 每个请求一个虚拟线程,数量可以轻松达到百万级
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
handleRequest(); // IO 阻塞时虚拟线程自动让出载体线程
});
}
}java虚拟线程由 JVM 调度而非操作系统,它们在遇到阻塞 IO 时会自动挂起并释放底层的平台线程(carrier thread),让其他虚拟线程运行。一个虚拟线程的内存开销只有几百字节到几 KB,创建一百万个毫无压力。
这意味着你可以回到“一个请求一个线程”的简单编程模型,同时获得异步编程的吞吐量。不再需要 CompletableFuture 的回调地狱,不再需要响应式编程的学习曲线。
JDK 25 的额外改进: JDK 24 的 JEP 491 解决了虚拟线程在
synchronized块中被“钉住”(pinning)的问题。到 JDK 25,虚拟线程在 synchronized 块中阻塞时也能正常让出载体线程,这意味着你不必急着把所有synchronized改成ReentrantLock。
2.2 Scoped Values(JEP 506,JDK 25 转正)⭐#
用过 ThreadLocal 的人都知道它的问题:忘记 remove() 就内存泄漏,线程池复用时值会串,虚拟线程场景下更是灾难——你不可能给百万个虚拟线程每个都分配一份 ThreadLocal 副本。
Scoped Values 是 ThreadLocal 的现代替代品:
// 声明一个 ScopedValue(通常是 static final)
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
// 在调用链入口绑定值
ScopedValue.where(CURRENT_USER, "alice").run(() -> {
// 整个调用链中都能读到这个值
handleRequest();
});
void handleRequest() {
// 任何深度的方法调用都能读取,无需参数传递
String user = CURRENT_USER.get(); // "alice"
queryDatabase(user);
}java与 ThreadLocal 的关键区别:
-
不可变:绑定后不能修改,杜绝了跨线程的脏读问题
-
有界生命周期:
run()结束后绑定自动消失,不存在内存泄漏 -
虚拟线程友好:没有 per-thread 存储的开销
-
结构化:与结构化并发天然配合,子任务自动继承父任务的绑定
如果你在 Web 框架中用 ThreadLocal 传递用户上下文、请求 ID 或租户信息,Scoped Values 是正确的替代方案。
2.3 结构化并发(JEP 505,第五次预览)#
结构化并发还在预览阶段(第五次了,API 经历了大改),但方向已经清晰。它的核心理念是:并发子任务的生命周期应该与启动它们的代码块对齐。
// JDK 25 的结构化并发(预览 API,可能继续变化)
try (var scope = StructuredTaskScope.open()) {
var userTask = scope.fork(() -> fetchUser(userId));
var orderTask = scope.fork(() -> fetchOrder(orderId));
scope.join(); // 等待所有子任务完成
return new UserOrder(userTask.get(), orderTask.get());
}
// scope 结束时,所有未完成的子任务都会被取消java这解决了传统 ExecutorService.submit() 的几个痛点:子任务抛异常时其他子任务不会被取消(资源浪费)、父线程被中断时子任务仍在运行(线程泄漏)、调试时看不到任务之间的父子关系。
注意: 这仍然是预览特性,API 可能还会变。如果你要在生产环境使用,请做好跟随版本升级调整代码的准备。
三、模式匹配与数据建模:告别冗长的类型判断#
JDK 8 到 JDK 25 之间,Project Amber 交付了一系列相互关联的语言特性。它们单独看都是语法糖,组合起来则构成了一种全新的 Java 编程范式。
3.1 Record 类(JDK 16 转正)#
在 JDK 8 中,一个简单的数据载体需要写一堆样板代码:
// JDK 8 的数据类 —— Lombok 的 @Data 就是为了拯救这种代码
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) { /* 十几行 */ }
@Override
public int hashCode() { /* 又是几行 */ }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}javaRecord 让这一切缩减到一行:
// JDK 16+ —— 编译器自动生成构造器、访问器、equals、hashCode、toString
public record Point(int x, int y) {}javaRecord 是不可变的,字段隐式 final。它们不是 Lombok 的替代品(Record 不支持继承和可变字段),而是语言层面对“数据就是数据”这一理念的正式支持。
3.2 密封类(JDK 17 转正)+ Switch 模式匹配(JDK 21 转正)⭐#
密封类限定了谁可以继承一个类或实现一个接口:
// 定义一个密封的形状层次结构
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}java配合 switch 模式匹配,你可以写出类型安全且无需 default 分支的逻辑:
// JDK 21+ 的模式匹配 switch —— 编译器确保你覆盖了所有子类
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// 不需要 default!编译器知道 Shape 只有这三个实现
};
}java对比 JDK 8 的写法:
// JDK 8 —— instanceof 级联,忘了某个类型?编译器不会告诉你
double area(Shape shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.getRadius() * c.getRadius();
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.getWidth() * r.getHeight();
} else if (shape instanceof Triangle) {
Triangle t = (Triangle) shape;
return 0.5 * t.getBase() * t.getHeight();
}
throw new IllegalArgumentException("未知形状");
}javaRecord + 密封类 + 模式匹配,三者组合在一起,让 Java 终于拥有了类似代数数据类型(ADT)的能力。这对于领域建模、协议解析、AST 处理等场景意义重大。
3.3 Record 模式解构(JDK 21 转正)#
模式匹配不止于类型判断,还能直接解构 Record 的字段:
// 直接解构 Record 的字段,无需中间变量
if (shape instanceof Circle(var radius)) {
System.out.println("半径: " + radius);
}
// 在 switch 中嵌套解构
String describe(Shape shape) {
return switch (shape) {
case Circle(var r) when r > 100 -> "大圆,半径 " + r;
case Circle(var r) -> "小圆,半径 " + r;
case Rectangle(var w, var h) -> w + " x " + h + " 的矩形";
case Triangle(var b, var h) -> "底 " + b + " 高 " + h + " 的三角形";
};
}java3.4 原始类型模式匹配(JEP 507,第三次预览)#
JDK 25 进一步将模式匹配扩展到原始类型:
// JDK 25 预览 —— instanceof 和 switch 支持 int、long 等原始类型
void processValue(Object obj) {
if (obj instanceof int i) {
System.out.println("整数: " + i);
}
}
// switch 中的原始类型模式
String classify(int statusCode) {
return switch (statusCode) {
case 200 -> "OK";
case int c when c >= 400 && c < 500 -> "客户端错误";
case int c when c >= 500 -> "服务端错误";
default -> "其他: " + statusCode;
};
}java这个特性仍在预览阶段,但方向是让所有类型(包括原始类型)都能参与模式匹配,最终实现统一的类型匹配语义。
四、文本与数据处理的进化#
4.1 文本块(JDK 15 转正)#
JDK 8 中拼接多行字符串是一种折磨:
// JDK 8 —— 转义和拼接让 JSON、SQL、HTML 面目全非
String json = "{\n"
+ " \"name\": \"Alice\",\n"
+ " \"age\": 30\n"
+ "}";java文本块用三引号解决了这个问题:
// JDK 15+ —— 所见即所得
String json = """
{
"name": "Alice",
"age": 30
}
""";java缩进由结尾 """ 的位置决定。这对于嵌入 SQL、JSON、HTML、正则表达式等场景非常实用。
4.2 Stream Gatherers(JDK 24 转正)#
JDK 8 的 Stream API 是一次飞跃,但中间操作只有 map、filter、flatMap 等固定集合。JDK 24 引入的 Gatherers 允许你定义自定义的中间操作:
// 使用内置的 Gatherer —— 窗口滑动
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
// 结果: [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
// 固定大小分组
List<List<Integer>> groups = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowFixed(2))
.toList();
// 结果: [[1, 2], [3, 4], [5]]javaGatherers 是 Stream API 的 Collector 在中间操作层面的对应物——它让 Stream 的表达能力从“够用”变成了“完备”。
五、运行时与性能:JDK 25 的重头戏#
JDK 25 的 18 个 JEP 中,有将近一半集中在运行时性能优化上。如果你的应用对内存、启动速度或可观测性有要求,这些特性值得关注。
5.1 紧凑对象头(JEP 519)⭐#
每个 Java 对象都有一个对象头(object header),用于存储锁状态、GC 年龄、类型指针等元信息。在 64 位 JVM 上,这个头通常占 96 到 128 位(12 到 16 字节)。
JEP 519 将对象头压缩到 64 位(8 字节)。这意味着每个对象节省 4–8 字节。听起来不多?但在一个有数百万对象的 Java 堆中,SPECjbb2015 基准测试显示堆内存占用减少约 22%,CPU 消耗减少约 8%。
JDK 25 中该特性已从实验转为正式产品特性,但默认关闭,需要手动开启:
java -XX:+UseCompactObjectHeaders MyAppbash对于微服务、容器化部署等内存敏感场景,这是一个立竿见影的优化手段。
5.2 AOT 缓存(JEP 483 + JEP 514 + JEP 515)#
Project Leyden 致力于缩短 Java 应用的启动时间。JDK 25 在这个方向上迈出了重要几步:
-
JEP 483(JDK 24):提前完成类的加载和链接,并将结果缓存
-
JEP 514(JDK 25):简化 AOT 缓存的命令行使用方式,引入
-XX:AOTCacheOutput=<file> -
JEP 515(JDK 25):记录方法级别的执行画像,为后续的 Profile-Guided AOT 编译奠基
现在,你可以用两步生成和使用 AOT 缓存:
# 训练运行:生成 AOT 缓存
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.Main
# 生产运行:使用 AOT 缓存加速启动
java -XX:AOTCache=app.aot -cp app.jar com.example.Mainbash对于 Spring Boot 等启动较重的框架应用,这能显著缩短冷启动时间。
5.3 Generational Shenandoah(JEP 521)#
Shenandoah 是一个以低延迟为目标的垃圾回收器,此前一直是非分代的。JDK 25 为它加入了分代支持,让年轻代对象可以被更高效地回收,与 G1 和 ZGC 的分代能力看齐。
java -XX:+UseShenandoahGC MyAppbash如果你是 G1 用户(JDK 9+ 的默认 GC),这不要求你做任何改变。但如果你对尾延迟(tail latency)要求极高,分代 Shenandoah 是新增的一个有力选项。
5.4 JFR 三件套(JEP 509 / 518 / 520)#
Java Flight Recorder(JFR)在 JDK 25 获得了三个增强:
-
CPU-Time Profiling(JEP 509,实验性,仅 Linux):记录方法级别的实际 CPU 时间消耗,而非仅仅采样。对于分析 IO 密集与 CPU 密集的混合负载尤其有用。
-
Cooperative Sampling(JEP 518):应用可以主动标记安全采样点,减少采样对性能的干扰,提高数据精度。
-
Method Timing & Tracing(JEP 520):记录每个方法调用的精确计时,支持完整的调用链重建。
这三个特性让 JFR 从“够用的监控工具”进化为“接近商业 APM 的可观测方案”。
六、安全与加密#
6.1 密钥派生函数 API(JEP 510)#
JDK 25 将 PBKDF2、scrypt、HKDF 等密钥派生函数纳入标准 API。以前你可能需要引入 Bouncy Castle 等第三方库来做密码哈希,现在标准库就能搞定。
6.2 PEM 编码 API(JEP 470,预览)#
处理 PEM 格式的证书和密钥(-----BEGIN PUBLIC KEY-----)不再需要手动解析 Base64。新的 API 让读写 PEM 编码的加密对象变得像读写字符串一样简单。
6.3 抗量子密码算法(JDK 24 引入)#
JDK 24 已经引入了基于模格(Module-Lattice)的数字签名算法(JEP 497,ML-DSA)和密钥封装机制(JEP 496,ML-KEM),为后量子时代做准备。这些算法在 JDK 25 中继续可用。
七、其他值得知道的变化#
从 JDK 8/11 直接升级还会一次性获得这些:
-
var** 局部变量类型推断**(JDK 10):var list = new ArrayList<String>()替代冗长的类型声明 -
switch** 表达式**(JDK 14 转正):switch不再只是语句,还能返回值,箭头语法取代 fall-through -
instanceof** 模式匹配**(JDK 16 转正):if (obj instanceof String s)一步完成判断和转型 -
未命名变量与模式(JDK 22):用
_显式标记不使用的变量,如catch (Exception _)或case Point(var x, _) -
默认 UTF-8 编码(JDK 18):不再有跨平台的编码问题
-
Foreign Function & Memory API(JDK 22 转正):JNI 的现代替代品,类型安全地调用本地代码
-
Class-File API(JDK 24 转正):标准库内置的字节码操作 API,减少对 ASM 等第三方库的依赖
-
永久禁用 Security Manager(JDK 24):如果你的代码依赖 SecurityManager,必须在升级前重构
-
Markdown Javadoc 注释(JDK 23):文档注释可以直接用 Markdown 写了
-
多文件源码程序(JDK 22):
java Main.java可以自动编译并运行依赖的其他源文件
迁移注意事项:
-
32 位 x86 端口已完全移除(JEP 503),如果你还在 32 位系统上运行 Java,这是终点
-
sun.misc.Unsafe** 的内存访问方法已弃用**(JEP 471/498),应迁移到VarHandle或 Foreign Memory API -
-XX:+UseCompressedClassPointers** 已弃用**,被紧凑对象头(JEP 519)取代 -
部分旧的 JMX 系统属性已移除,升级前务必检查 Release Notes
八、该不该升级?#
如果你还在 JDK 8,答案是应该,但不必急。JDK 25 是 LTS,这意味着它会获得长期的安全补丁和 bug 修复,是一个稳妥的升级目标。但升级本身需要投入测试资源,尤其是要检查:
-
依赖兼容性:确保你使用的框架和库支持 JDK 25(Spring Boot 3.x、Jakarta EE 等已经就绪)
-
模块系统的影响:JDK 9 引入的模块系统会限制对内部 API 的反射访问,
--add-opens可能需要配置 -
已移除的 API:Security Manager、Nashorn JavaScript 引擎、旧的 Applet API 等早已不在
从 JDK 11 升级到 JDK 25 相对平滑得多,模块系统的适配工作大多已经完成。
最后一条建议:预览特性不要用于生产。JDK 25 中的结构化并发(第五次预览)、Stable Values(首次预览)、PEM 编码 API(首次预览)和原始类型模式匹配(第三次预览)都可能在后续版本中发生 API 变化。转正的特性才是你升级的真正红利。
JDK 25 官方 JEP 完整列表:openjdk.org/projects/jdk/25 ↗ JDK 25 Release Notes:oracle.com/java/technologies/javase/25-relnote-issues.html ↗ 自 JDK 21 以来的所有 JEP:openjdk.org/projects/jdk/25/jeps-since-jdk-21 ↗