zdzy

Back

从 JDK 8 到 JDK 25:一份迟到但值得的升级指南Blur image

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!");
    }
}
java

JDK 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;
    }
}
java

JDK 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.*;
java

JDK 25 引入了模块级导入:

import module java.base;
java

这一行等价于导入 java.base 模块导出的所有包。对于日常开发来说,这减少了大量的 import 语句。但要注意歧义问题——如果同时导入 java.basejava.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();
    });
}
java

200 个线程已经不少了。如果每个请求要等 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 中,一个简单的数据载体需要写一堆样板代码:

Record 让这一切缩减到一行:

// JDK 16+ —— 编译器自动生成构造器、访问器、equals、hashCode、toString
public record Point(int x, int y) {}
java

Record 是不可变的,字段隐式 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("未知形状");
}
java

Record + 密封类 + 模式匹配,三者组合在一起,让 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 + " 的三角形";
    };
}
java

3.4 原始类型模式匹配(JEP 507,第三次预览)#

JDK 25 进一步将模式匹配扩展到原始类型:

这个特性仍在预览阶段,但方向是让所有类型(包括原始类型)都能参与模式匹配,最终实现统一的类型匹配语义。

四、文本与数据处理的进化#

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 是一次飞跃,但中间操作只有 mapfilterflatMap 等固定集合。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]]
java

Gatherers 是 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 MyApp
bash

对于微服务、容器化部署等内存敏感场景,这是一个立竿见影的优化手段。

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.Main
bash

对于 Spring Boot 等启动较重的框架应用,这能显著缩短冷启动时间。

5.3 Generational Shenandoah(JEP 521)#

Shenandoah 是一个以低延迟为目标的垃圾回收器,此前一直是非分代的。JDK 25 为它加入了分代支持,让年轻代对象可以被更高效地回收,与 G1 和 ZGC 的分代能力看齐。

java -XX:+UseShenandoahGC MyApp
bash

如果你是 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 修复,是一个稳妥的升级目标。但升级本身需要投入测试资源,尤其是要检查:

  1. 依赖兼容性:确保你使用的框架和库支持 JDK 25(Spring Boot 3.x、Jakarta EE 等已经就绪)

  2. 模块系统的影响:JDK 9 引入的模块系统会限制对内部 API 的反射访问,--add-opens 可能需要配置

  3. 已移除的 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

从 JDK 8 到 JDK 25:一份迟到但值得的升级指南
https://blog.zdzy.xyz/blog/jdk25-new-features
Author Easton
Published at September 18, 2026