Java 25 新特性总结

发布于 2026-10-01 21:00 9796 字 49 min read

issyuu avatar

issyuu

静水深流,湖心小筑(Lakeheart Retreat),在静谧水畔凝结思考,谱写日常的歌。

用可以直接运行的示例代码和运行结果,讲解 Java 25(LTS)的 18 个 JEP。让 Java 更容易上手的语言特性、正式发布的 Scoped Values,以及对象头压缩、AOT 缓存、分代 Shenandoah 都用实测数据深入分析,并整理了各特性到 JDK 27 为止的后续进展。

前言

Java 25 是 2025 年 9 月发布的 LTS(长期支持)版本。从 Java 21 起两年间、JDK 22〜25 的成果都集中到了这个版本里。

如果说 Java 21 用虚拟线程改变了“并发的写法”,那么 Java 25 的主题大致有两个。一个是 上手更简单:不用写 class,也不用写 public static void main(String[] args),就能开始写程序。另一个是 运行时效率:不改一行代码,就能减少内存占用和启动时间的选项变多了。

本文 一边实际运行,一边 介绍 Java 25 的 18 个 JEP。所有示例代码都是单个文件,不依赖任何外部库。和性能有关的特性,会用实测的数字来确认效果。每个特性的最后都附上 「后续进展」,补充到 JDK 27(2026 年 9 月 GA)为止的变化。

验证环境与示例的运行方法

  • JDK:OpenJDK 25(25.0.4)
  • 操作系统 / CPU:macOS(Apple Silicon,8 核)
  • 直接运行源文件(JEP 330),不需要先编译(只有 AOT 缓存的示例需要打 JAR 包)。
java CompactSourceDemo.java                        # 正式特性
java --enable-preview StableValueDemo.java         # 预览特性
  • 在 JDK 25 中直接运行源文件时,只要加 --enable-preview 就能启用预览特性(Java 21 还需要 --source 21)。
  • 示例代码也放在 GitHub 的 smile-lxy/java-new-features 上。代码注释用日语、中文、英语三种语言写成,本文只保留中文注释。
  • 每个示例开头的注释(// Run:)写有运行命令。
  • 输出中的时间、内存大小、密钥的值,会随运行环境和每次运行而变化。

全貌:Java 25 的 18 个 JEP

JEP特性在 Java 25 中的状态后续进展(〜JDK 27)
470PEM Encodings of Cryptographic Objects预览JDK 27 仍是第 3 次预览
502Stable Values预览改名为 Lazy Constants,JDK 27 为第 3 次预览
503删除 32 位 x86 移植删除―
505Structured Concurrency第 5 次预览JDK 27 仍是第 7 次预览
506Scoped Values正式―
507Primitive Types in Patterns第 3 次预览JDK 27 仍是第 5 次预览
508Vector API第 10 次孵化JDK 27 仍是第 12 次孵化
509JFR CPU-Time Profiling实验性(仅 Linux)―
510Key Derivation Function API正式―
511Module Import Declarations正式―
512Compact Source Files and Instance Main Methods正式―
513Flexible Constructor Bodies正式―
514简化 AOT 命令行正式JDK 26 起任何 GC 都能缓存对象
515AOT 方法性能分析正式同上
518JFR Cooperative Sampling正式―
519Compact Object Headers正式(可选)JDK 27 默认启用
520JFR Method Timing & Tracing正式―
521Generational Shenandoah正式(可选)―

这些可以大致分为 4 类。

flowchart LR
    J25["Java 25"] --> L["语言特性"]
    J25 --> C["并发"]
    J25 --> A["类库・<br/>安全"]
    J25 --> R["运行时・<br/>监控"]
    L --> L1["Compact Source Files(512)"]
    L --> L2["Module Import(511)"]
    L --> L3["Flexible Constructor(513)"]
    L --> L4["Primitive Patterns(507)"]
    C --> C1["Scoped Values(506)"]
    C --> C2["Structured Concurrency(505)"]
    A --> A1["Stable Values(502)"]
    A --> A2["KDF API(510)"]
    A --> A3["PEM(470)"]
    A --> A4["Vector API(508)"]
    R --> R1["Compact Object Headers(519)"]
    R --> R2["AOT 缓存(514・515)"]
    R --> R3["分代 Shenandoah(521)"]
    R --> R4["JFR(509・518・520)"]
    classDef star fill:#fde68a,stroke:#b45309,color:#78350f
    class L1,C1,R1,R2,R3 star

本文按照 语言特性 → 并发 → 类库・安全 → 运行时・监控 的顺序介绍。正式发布的特性中影响较大的(黄色)会详细展开,运行时的 3 项会用实测来确认效果。

语言特性

Compact Source Files and Instance Main Methods(JEP 512)

现在 不写类声明,只写方法就能开始一个程序 了。这是 Java 21 的 JEP 445(Unnamed Classes and Instance Main Methods)在改了名字和规格、经历 4 次预览之后正式发布的结果。

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}
void main() {
    IO.println("Hello");
}
// JEP 512: Compact Source Files and Instance Main Methods
// Run: java CompactSourceDemo.java

// 不需要 class 声明,也不需要 public static 和 String[] args。java.base 的类不用 import 就能用

final List<String> fruits = List.of("apple", "banana", "cherry");

String shout(String s) {
    return s.toUpperCase() + "!";
}

void main() {
    // IO.println 是 System.out.println 的简化版(java.lang.IO,JDK 25 新增)
    IO.println("Hello, Java 25!");
    for (String f : fruits) {
        IO.println(shout(f));
    }

    // List、Map、Collectors 等都通过自动 import java.base 模块而可用
    Map<Integer, List<String>> byLength = fruits.stream().collect(Collectors.groupingBy(String::length));
    IO.println("by length: " + byLength);

    // 其实编译器生成了一个隐式类
    IO.println("class name: " + getClass().getName());
}
Hello, Java 25!
APPLE!
BANANA!
CHERRY!
by length: {5=[apple], 6=[banana, cherry]}
class name: CompactSourceDemo

要点有 3 个。

  1. 隐式类:写在顶层的字段和方法,会成为编译器自动生成的类(类名与文件名相同)的成员。
  2. 实例 main:不再需要 static、public 和参数。如果 main 是实例方法,JVM 会先创建实例再调用它。
  3. 自动 import java.base 与 IO 类:在精简源文件中,会隐式执行下一节介绍的 import module java.base;。List、Map、Collectors 都不用 import 就能用。新的 java.lang.IO 类可以把 System.out.println 简写成 IO.println。

这看起来是面向初学者的特性,但写一些小脚本或验证代码时也很方便。程序变大之后,只要补上 class 声明,就能变回普通的类。

在 JDK 23、24 的预览中,println 等方法是自动静态导入的,正式版最终定为写成 IO.println 的形式。另外,在 Java 21 中它叫“未命名类(unnamed class)”,JDK 22 改称为“隐式类(implicit class)”。

Module Import Declarations(JEP 511)

用 import module,可以 一次性 import 某个模块导出的所有包 了。

// JEP 511: Module Import Declarations
// Run: java ModuleImportDemo.java

// 一次性 import 某个模块导出的所有包
import module java.base;      // java.util, java.util.stream, java.time, java.nio.file ...
import module java.net.http;  // java.net.http

// java.desktop 里也有 java.awt.List,List 会产生歧义。单类型 import 优先,用它来解决
import module java.desktop;
import java.util.List;

public class ModuleImportDemo {

    public static void main(String[] args) {
        // java.util、java.time、java.net.http 的类都没有单独 import 就用上了
        List<String> names = Stream.of("b", "a", "c").sorted().toList();
        LocalDate date = LocalDate.of(2025, 9, 16);
        HttpClient.Version version = HttpClient.Version.HTTP_2;
        System.out.println("names   = " + names);
        System.out.println("JDK 25  = " + date + " (" + date.getDayOfWeek() + ")");
        System.out.println("version = " + version);

        // java.desktop 的类也可以用(List 除外)
        Point p = new Point(3, 4);
        System.out.println("point   = " + p.x + "," + p.y + " (distance " + p.distance(0, 0) + ")");
        System.out.println("List is " + List.class.getName());
    }
}
names   = [a, b, c]
JDK 25  = 2025-09-16 (TUESDAY)
version = HTTP_2
point   = 3,4 (distance 5.0)
List is java.util.List

一行 import module java.base;,就能用上 java.util、java.util.stream、java.time、java.nio.file 等 58 个包。

需要注意的是 名字冲突。java.desktop 里有 java.awt.List,和 java.base 一起 import 时,List 就会产生歧义。删掉示例中的 import java.util.List;,会出现下面的错误。

ModuleImportDemo.java:15: error: reference to List is ambiguous
        List<String> names = Stream.of("b", "a", "c").sorted().toList();
        ^
  both class java.awt.List in java.awt and interface java.util.List in java.util match

只指定一个类型的普通 import 优先于 import module,所以用它就能解决。

即使自己的代码不是模块(没有 module-info.java),也可以使用 import module。在业务代码中,它会让人难以看出某个类来自哪里,所以和通配符 import 一样,要看场合使用。它和原型代码、上一节的精简源文件很搭。

Flexible Constructor Bodies(JEP 513)

在构造器中,现在可以 在 super(...) 或 this(...) 之前写代码 了。可以先校验参数,或者先计算要传给父类的值。

// JEP 513: Flexible Constructor Bodies
// Run: java FlexibleConstructorDemo.java

public class FlexibleConstructorDemo {

    static class Shape {
        Shape() {
            // 在父类构造器里调用被重写的方法(常见的陷阱)
            System.out.println("  Shape(): " + describe());
        }
        String describe() { return "shape"; }
    }

    // 以前:super() 必须写在第一行,字段只能在 super() 之后赋值
    static class OldCircle extends Shape {
        private final double radius;
        OldCircle(double radius) {
            super();
            this.radius = radius;
        }
        @Override String describe() { return "circle r=" + radius; }
    }

    // JDK 25:可以在 super() 之前校验参数、初始化字段
    static class NewCircle extends Shape {
        private final double radius;
        NewCircle(double radius) {
            if (radius <= 0) {
                throw new IllegalArgumentException("radius must be positive: " + radius);
            }
            this.radius = radius;
            super();
        }
        @Override String describe() { return "circle r=" + radius; }
    }

    // 在 super() 之前计算要传给父类的值(以前只能放到 static 方法里)
    static class Base {
        final String id;
        Base(String id) { this.id = id; }
    }
    static class User extends Base {
        User(String email) {
            String normalized = email.trim().toLowerCase();
            if (!normalized.contains("@")) {
                throw new IllegalArgumentException("not an email: " + email);
            }
            super(normalized);
        }
    }

    public static void main(String[] args) {
        System.out.println("new OldCircle(2):");
        new OldCircle(2);
        System.out.println("new NewCircle(2):");
        new NewCircle(2);

        try {
            new NewCircle(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("new NewCircle(-1): " + e.getMessage());
        }
        System.out.println("new User(\"  Alice@Example.COM \").id = " + new User("  Alice@Example.COM ").id);
    }
}
new OldCircle(2):
  Shape(): circle r=0.0
new NewCircle(2):
  Shape(): circle r=2.0
new NewCircle(-1): radius must be positive: -1.0
new User("  Alice@Example.COM ").id = alice@example.com

请注意 OldCircle 的输出。父类构造器 Shape() 调用了被重写的 describe()。这时子类字段 radius 还没有赋值,所以看到的是 0.0。这是 Java 中的经典陷阱。

在 NewCircle 中,可以在 super() 之前写 this.radius = radius;,所以父类构造器也能看到正确的值。

flowchart TB
    subgraph P["序言(super 之前)"]
        P1["校验参数"] --> P2["初始化字段"]
        P2 --> P3["计算传给父类的值"]
    end
    P3 --> S["super(...)"]
    S --> E["尾声(super 之后)<br/>可以自由使用 this"]

不过,在 super() 之前(序言部分),不能做需要使用尚未初始化的 this 的操作。调用实例方法、读取还没初始化的字段,都会编译失败。能做的只有给字段 赋值,以及不涉及 this 的计算。

Primitive Types in Patterns, instanceof, and switch(JEP 507・第 3 次预览)

模式匹配中可以使用 基本类型 了。Java 21 正式发布的模式匹配只支持引用类型。

// JEP 507: Primitive Types in Patterns, instanceof, and switch (third preview in JDK 25)
// Run: java --enable-preview PrimitivePatternsDemo.java

public class PrimitivePatternsDemo {

    // instanceof int:检查值能否无损地转换为 int
    static String fitsInInt(long value) {
        if (value instanceof int i) {
            return value + " fits in int: " + i;
        }
        return value + " does not fit in int";
    }

    // double 也一样。没有小数部分且在范围内,就能匹配 int
    static String classify(double d) {
        return switch (d) {
            case int i -> "integral " + i;
            case float f -> "fits in float " + f;
            case double x -> "needs double " + x;
        };
    }

    // 在 switch 中组合基本类型模式和 when 守卫
    static String httpStatus(int code) {
        return switch (code) {
            case 200 -> "OK";
            case 404 -> "Not Found";
            case int c when c >= 500 -> "server error " + c;
            case int c -> "other " + c;
        };
    }

    // boolean、long 等也可以作为 switch 的选择器了
    static String onOff(boolean b) {
        return switch (b) {
            case true -> "ON";
            case false -> "OFF";
        };
    }

    public static void main(String[] args) {
        System.out.println(fitsInInt(42L));
        System.out.println(fitsInInt(10_000_000_000L));
        System.out.println(classify(3.0));
        System.out.println(classify(0.5));
        System.out.println(classify(0.1));
        System.out.println(httpStatus(200) + " / " + httpStatus(503) + " / " + httpStatus(302));
        System.out.println(onOff(true) + " / " + onOff(false));

        // 以前的强制转换会悄悄地破坏值
        System.out.println("(int) 10_000_000_000L = " + (int) 10_000_000_000L);
    }
}
42 fits in int: 42
10000000000 does not fit in int
integral 3
fits in float 0.5
needs double 0.1
OK / server error 503 / other 302
ON / OFF
(int) 10_000_000_000L = 1410065408

value instanceof int i 检查的是:“value 能否 无损地 转换为 int”。10_000_000_000L 放不进 int,所以不匹配。以前的强制转换 (int) 10_000_000_000L 不会报错,而是返回 1410065408 这个被破坏的值。能安全地写出这种判断,正是这个特性的价值。

switch 的选择器也可以使用 boolean、long、float、double 了,还可以像 case int c when c >= 500 这样和守卫组合使用。

后续进展:JDK 26 为第 4 次预览(JEP 530),JDK 27 为第 5 次预览(JEP 532),目前还没有正式发布。

并发

Scoped Values(JEP 506)

在 Java 21 中以预览身份登场的 Scoped Values,在第 5 次时 正式发布 了。它是用来代替 ThreadLocal、把值传递到调用层级深处的机制。

// JEP 506: Scoped Values
// Run: java ScopedValueDemo.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ScopedValueDemo {

    record User(String name, String role) {}

    // 不通过参数传递,而是共享“当前请求的用户”
    static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
    static final ThreadLocal<User> TL_USER = new ThreadLocal<>();

    static void controller() {
        service();
    }

    static void service() {
        // 在调用层级很深的地方读取值
        User u = CURRENT_USER.get();
        System.out.println("  service: user=" + u.name() + ", role=" + u.role());
    }

    public static void main(String[] args) throws Exception {
        // ① 值只在 where(...).run(...) 内部可见
        System.out.println("(1) bind and read");
        ScopedValue.where(CURRENT_USER, new User("alice", "admin")).run(ScopedValueDemo::controller);
        System.out.println("  after run: isBound=" + CURRENT_USER.isBound());

        // ② 可以嵌套地临时绑定另一个值,退出后自动恢复
        System.out.println("(2) nested rebinding");
        ScopedValue.where(CURRENT_USER, new User("alice", "admin")).run(() -> {
            ScopedValue.where(CURRENT_USER, new User("guest", "viewer")).run(ScopedValueDemo::service);
            service();
        });

        // ③ 用 call() 可以返回值
        String greeting = ScopedValue.where(CURRENT_USER, new User("bob", "user"))
                .call(() -> "hello, " + CURRENT_USER.get().name());
        System.out.println("(3) call() -> " + greeting);

        // ④ ThreadLocal 如果忘了 remove(),在线程池中值会泄漏给下一个任务
        System.out.println("(4) ThreadLocal vs ScopedValue on a pooled thread");
        try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
            pool.submit(() -> TL_USER.set(new User("carol", "admin"))).get();
            pool.submit(() -> System.out.println("  next task sees ThreadLocal: " + TL_USER.get())).get();
            pool.submit(() -> ScopedValue.where(CURRENT_USER, new User("carol", "admin")).run(() -> {})).get();
            pool.submit(() -> System.out.println("  next task sees ScopedValue: "
                    + (CURRENT_USER.isBound() ? CURRENT_USER.get() : "(unbound)"))).get();
        }

        // ⑤ 在绑定范围之外调用 get() 会抛出异常
        try {
            CURRENT_USER.get();
        } catch (java.util.NoSuchElementException e) {
            System.out.println("(5) get() outside: " + e.getClass().getSimpleName());
        }
    }
}
(1) bind and read
  service: user=alice, role=admin
  after run: isBound=false
(2) nested rebinding
  service: user=guest, role=viewer
  service: user=alice, role=admin
(3) call() -> hello, bob
(4) ThreadLocal vs ScopedValue on a pooled thread
  next task sees ThreadLocal: User[name=carol, role=admin]
  next task sees ScopedValue: (unbound)
(5) get() outside: NoSuchElementException

和 ThreadLocal 的区别如下。

ThreadLocalScopedValue
修改值随时可以用 set() 修改不能修改(只能在嵌套中临时绑定另一个值)
有效期一直保留到 remove()退出 run() / call() 时自动消失
传给子线程InheritableThreadLocal 需要复制不用复制就能传给 StructuredTaskScope 的子任务
主要用途每个线程的缓存等以请求为单位的上下文(用户、事务等)

正如 ④ 的输出所示,ThreadLocal 如果忘了 remove(),在线程池中 值会泄漏给下一个任务。如果是认证信息,就可能酿成严重事故。ScopedValue 的绑定范围由语法结构决定,不会出现这类 bug。

和 Java 21 的预览版相比,API 有一些变化。静态方法 runWhere()、callWhere() 以及 where(...).get(...) 被删除,统一为在 where(...) 后面调用 run() 或 call() 的形式。call() 接收可以抛出受检异常的 ScopedValue.CallableOp。

Structured Concurrency(JEP 505・第 5 次预览)

把多个子任务当作 一个工作单元 来处理的 API。基本思想和 Java 21 相同,但在 JDK 25 的第 5 次预览中,API 被大幅重新设计。

Java 21(JEP 453)Java 25(JEP 505)
创建作用域new StructuredTaskScope.ShutdownOnFailure() 等子类StructuredTaskScope.open(joiner)
成功与失败的策略通过子类选择通过 Joiner 选择
获取结果join() 之后调用 throwIfFailed() / result()join() 直接返回结果或抛出异常
超时joinUntil(Instant)在 open() 的配置中设置 withTimeout(Duration)
// JEP 505: Structured Concurrency (fifth preview in JDK 25)
// Run: java --enable-preview StructuredConcurrencyDemo.java

import java.time.Duration;
import java.util.List;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;

public class StructuredConcurrencyDemo {

    static long t0;

    static void log(String message) {
        System.out.printf("%5d ms  %s%n", (System.nanoTime() - t0) / 1_000_000, message);
    }

    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    // 耗时 ms 毫秒的处理。被中断时输出日志
    static String work(String name, long ms, boolean fail) throws InterruptedException {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            log(name + ": interrupted");
            throw e;
        }
        if (fail) {
            log(name + ": failed");
            throw new IllegalStateException(name + " failed");
        }
        log(name + ": done");
        return name;
    }

    public static void main(String[] args) throws Exception {
        // ① 默认的 Joiner:等待全部成功。只要有一个失败,就中断其余的
        System.out.println("=== 1. open(): one subtask fails ===");
        t0 = System.nanoTime();
        try (var scope = StructuredTaskScope.open()) {
            Subtask<String> user = scope.fork(() -> work("findUser", 100, true));
            Subtask<String> order = scope.fork(() -> work("fetchOrder", 1000, false));
            scope.join();
            log("never reached: " + user.get() + order.get());
        } catch (StructuredTaskScope.FailedException e) {
            log("caller: " + e.getCause().getMessage());
        }

        // ② 只使用最先成功的结果(例如多个镜像服务器竞速)
        System.out.println("=== 2. anySuccessfulResultOrThrow() ===");
        t0 = System.nanoTime();
        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow())) {
            scope.fork(() -> work("mirror A", 300, false));
            scope.fork(() -> work("mirror B", 100, false));
            scope.fork(() -> work("mirror C", 50, true));
            log("result: " + scope.join());
        }

        // ③ 一次性拿到所有结果
        System.out.println("=== 3. allSuccessfulOrThrow() ===");
        t0 = System.nanoTime();
        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow())) {
            for (String s : List.of("price", "stock", "review")) {
                scope.fork(() -> work(s, 100, false));
            }
            List<String> results = scope.join().map(Subtask::get).toList();
            log("results: " + results);
        }

        // ④ 给整个作用域设置超时
        System.out.println("=== 4. withTimeout(250 ms) ===");
        t0 = System.nanoTime();
        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
                cf -> cf.withTimeout(Duration.ofMillis(250)))) {
            scope.fork(() -> work("fast", 100, false));
            scope.fork(() -> work("slow", 1000, false));
            scope.join();
        } catch (StructuredTaskScope.TimeoutException e) {
            log("caller: TimeoutException");
        }

        // ⑤ fork 出来的子任务也会继承 ScopedValue
        System.out.println("=== 5. ScopedValue inheritance ===");
        t0 = System.nanoTime();
        ScopedValue.where(REQUEST_ID, "req-42").call(() -> {
            try (var scope = StructuredTaskScope.open()) {
                Subtask<String> a = scope.fork(() -> "subtask A sees " + REQUEST_ID.get());
                Subtask<String> b = scope.fork(() -> "subtask B sees " + REQUEST_ID.get());
                scope.join();
                log(a.get());
                log(b.get());
            }
            return null;
        });
    }
}
=== 1. open(): one subtask fails ===
  110 ms  findUser: failed
  120 ms  fetchOrder: interrupted
  120 ms  caller: findUser failed
=== 2. anySuccessfulResultOrThrow() ===
   58 ms  mirror C: failed
  108 ms  mirror B: done
  108 ms  mirror A: interrupted
  108 ms  result: mirror B
=== 3. allSuccessfulOrThrow() ===
  104 ms  stock: done
  104 ms  price: done
  105 ms  review: done
  107 ms  results: [price, stock, review]
=== 4. withTimeout(250 ms) ===
  107 ms  fast: done
  255 ms  slow: interrupted
  255 ms  caller: TimeoutException
=== 5. ScopedValue inheritance ===
    1 ms  subtask A sees req-42
    2 ms  subtask B sees req-42

把 ②(使用最先成功的结果)和 ④(超时)画成时间线,就是下面这样。

② 使用最先成功的结果 / ④ 250 ms 超时(实测,刻度单位:s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 100millisecond
    section (2) any
    mirror A 中断 :0, 108
    mirror B 成功 :active, 0, 108
    mirror C 失败 :crit, 0, 58
    section (4) timeout
    fast 成功 :active, 0, 107
    slow 中断 :0, 255

两种情况下,不再需要的子任务都由作用域自动中断。② 中 mirror B 成功时 mirror A 被中断,④ 中 slow 因 250 ms 的超时被中断。调用方不需要写任何取消处理。

主要的 Joiner 如下。

Joiner行为
(不指定)等待全部成功。只要有一个失败就中断其余的,并抛出 FailedException
allSuccessfulOrThrow()同上,并返回全部结果
anySuccessfulResultOrThrow()返回最先成功的结果,中断其余的
awaitAll()不论成功失败,等待全部结束

后续进展:JDK 26 的第 6 次预览(JEP 525)做了一些细微的 API 调整。anySuccessfulResultOrThrow() 改名为 anySuccessfulOrThrow(),allSuccessfulOrThrow() 改为返回结果列表,还新增了超时时也能返回结果的 Joiner.onTimeout()。JDK 27 仍是第 7 次预览(JEP 533),尚未正式发布。

类库・安全

Stable Values(JEP 502・预览)

表示 只能设置一次的值 的 API。它像 final 字段一样可以被 JIT 编译器当作常量优化,同时又能自由推迟初始化的时机。

以前,想要“把很重的初始化推迟到第一次使用时”,需要用双重检查锁或者 holder 类惯用法。

// JEP 502: Stable Values (preview in JDK 25)
// Run: java --enable-preview StableValueDemo.java

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class StableValueDemo {

    static final AtomicInteger CREATED = new AtomicInteger();

    static class Settings {
        Settings() {
            // 假装初始化很耗时(100 ms)
            CREATED.incrementAndGet();
            sleep(100);
        }
    }

    // “只能设置一次的值”。JIT 可以像 final 字段一样把它当作常量
    static final StableValue<Settings> SETTINGS = StableValue.of();

    static Settings settings() {
        // 如果还没设置就初始化。即使多个线程同时调用,也只初始化一次
        return SETTINGS.orElseSet(Settings::new);
    }

    // 作为 Supplier 使用的写法
    static final Supplier<String> GREETING = StableValue.supplier(() -> {
        System.out.println("  computing greeting...");
        return "hello";
    });

    // 每个元素都延迟初始化的列表
    static final List<Integer> SQUARES = StableValue.list(10, i -> {
        System.out.println("  computing square of " + i);
        return i * i;
    });

    static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("(1) 8 threads call settings() at the same time");
        Thread[] threads = new Thread[8];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = Thread.ofVirtual().start(StableValueDemo::settings);
        }
        for (Thread t : threads) t.join();
        System.out.println("  Settings created " + CREATED.get() + " time(s), isSet=" + SETTINGS.isSet());

        System.out.println("(2) supplier");
        System.out.println("  " + GREETING.get() + " / " + GREETING.get());

        System.out.println("(3) list");
        System.out.println("  SQUARES.get(3) = " + SQUARES.get(3));
        System.out.println("  SQUARES.get(3) = " + SQUARES.get(3));
        System.out.println("  SQUARES.get(7) = " + SQUARES.get(7));

        // 第二次设置会失败
        System.out.println("(4) trySet on a set value -> " + SETTINGS.trySet(new Settings()));
    }
}
(1) 8 threads call settings() at the same time
  Settings created 1 time(s), isSet=true
(2) supplier
  computing greeting...
  hello / hello
(3) list
  computing square of 3
  SQUARES.get(3) = 9
  SQUARES.get(3) = 9
  computing square of 7
  SQUARES.get(7) = 49
(4) trySet on a set value -> false

即使 8 个线程同时调用 settings(),Settings 也只会被创建 1 次。在 StableValue.list() 中,每个元素都在需要时才计算,而且只计算 1 次。

后续进展:在 JDK 26 的第 2 次预览(JEP 526)中改名为 Lazy Constants,API 也大幅精简。StableValue 变成了 LazyConstant,orElseSet()、trySet() 等底层方法被删除。延迟列表用 List.ofLazy(),延迟映射用 Map.ofLazy() 创建。JDK 27 仍是第 3 次预览(JEP 531)。上面的代码只适用于 JDK 25,在 JDK 26 及以后无法编译。

// JDK 26 及以后(预览)
static final LazyConstant<Settings> SETTINGS = LazyConstant.of(Settings::new);
static final List<Integer> SQUARES = List.ofLazy(10, i -> i * i);

Key Derivation Function API(JEP 510)

密钥派生函数(KDF)的标准 API。它从密钥交换等得到的秘密出发,安全地为不同用途生成各自的密钥。它是作为 Java 21 的 KEM API(JEP 452)和抗量子密码 HPKE 等所需的组件而加入的。

// JEP 510: Key Derivation Function API
// Run: java KdfDemo.java

import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.KDF;
import javax.crypto.SecretKey;
import javax.crypto.spec.HKDFParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class KdfDemo {

    public static void main(String[] args) throws Exception {
        // 通过密钥交换等得到的“原始秘密”(实际中应是随机值)
        byte[] ikm = HexFormat.of().parseHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
        byte[] salt = HexFormat.of().parseHex("000102030405060708090a0b0c");

        KDF hkdf = KDF.getInstance("HKDF-SHA256");

        // 从同一个秘密出发,按用途(info)派生出不同的密钥
        for (String purpose : new String[] {"encryption", "mac"}) {
            var params = HKDFParameterSpec.ofExtract()
                    .addIKM(new SecretKeySpec(ikm, "Generic"))
                    .addSalt(salt)
                    .thenExpand(purpose.getBytes(StandardCharsets.UTF_8), 32);
            SecretKey key = hkdf.deriveKey(purpose.equals("mac") ? "HmacSHA256" : "AES", params);
            System.out.printf("%-10s -> %s (%s, %d bytes)%n", purpose,
                    HexFormat.of().formatHex(key.getEncoded()), key.getAlgorithm(), key.getEncoded().length);
        }

        // 与 RFC 5869 测试用例 1 相同的输入:检查 OKM 是否与规范中的值一致
        byte[] info = HexFormat.of().parseHex("f0f1f2f3f4f5f6f7f8f9");
        byte[] okm = hkdf.deriveData(HKDFParameterSpec.ofExtract()
                .addIKM(new SecretKeySpec(ikm, "Generic")).addSalt(salt).thenExpand(info, 42));
        String expected = "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865";
        System.out.println("RFC 5869 test case 1 matches: " + HexFormat.of().formatHex(okm).equals(expected));
    }
}
encryption -> f6f274c198f909c7d84cd755655012f676a5ec8db9eefb176877df8e6120d359 (AES, 32 bytes)
mac        -> 4e3cb41f6fb908cd0b5bb6927bf6b9bec5cb1dd15eb440e1ef23d7c7dcbb27a9 (HmacSHA256, 32 bytes)
RFC 5869 test case 1 matches: true

从同一个秘密出发,只改变 info(用途),就能得到完全不同的密钥。最后一行确认了结果与 HKDF 规范 RFC 5869 的测试向量一致。以前要使用 HKDF,需要 Bouncy Castle 之类的外部库,或者自己实现。

PEM Encodings of Cryptographic Objects(JEP 470・预览)

在密钥、证书与 PEM 格式(以 -----BEGIN PUBLIC KEY----- 开头的文本)之间相互转换的 API。

// JEP 470: PEM Encodings of Cryptographic Objects (preview in JDK 25)
// Run: java --enable-preview PemDemo.java

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PEMDecoder;
import java.security.PEMEncoder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;

public class PemDemo {

    public static void main(String[] args) throws Exception {
        KeyPair pair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();

        // 把公钥转成 PEM 字符串(以前需要自己拼 Base64 和 BEGIN/END 行)
        String publicPem = PEMEncoder.of().encodeToString(pair.getPublic());
        System.out.print(publicPem);

        // 从 PEM 字符串还原为密钥对象
        PublicKey decoded = PEMDecoder.of().decode(publicPem, PublicKey.class);
        System.out.println("decoded: " + decoded.getAlgorithm() + ", equal = "
                + Arrays.equals(decoded.getEncoded(), pair.getPublic().getEncoded()));

        // 私钥可以用密码加密后再转成 PEM
        char[] password = "correct horse battery staple".toCharArray();
        String encryptedPem = PEMEncoder.of().withEncryption(password).encodeToString(pair.getPrivate());
        System.out.println(encryptedPem.lines().findFirst().orElseThrow() + " ... ("
                + encryptedPem.lines().count() + " lines)");
        PrivateKey privateKey = PEMDecoder.of().withDecryption(password).decode(encryptedPem, PrivateKey.class);
        System.out.println("decrypted: " + privateKey.getAlgorithm() + ", equal = "
                + Arrays.equals(privateKey.getEncoded(), pair.getPrivate().getEncoded()));
    }
}
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAPvSZ3v989RaF/zb9ffrrTOJEmMck1dMTBNLpMzBYVwA=
-----END PUBLIC KEY-----
decoded: EdDSA, equal = true
-----BEGIN ENCRYPTED PRIVATE KEY----- ... (6 lines)
decrypted: EdDSA, equal = true

PEM 是交换密钥和证书时最常用的格式,但以前 Java 的标准 API 无法直接处理它。需要自己做 Base64 编码、拼出 BEGIN / END 行,或者使用外部库。用密码加密的私钥也能处理。

后续进展:JDK 26 为第 2 次预览(JEP 524),JDK 27 为第 3 次预览(JEP 538)。

Vector API(JEP 508・第 10 次孵化)

用来在 Java 中显式编写利用 CPU SIMD 指令的向量运算的 API。这里分别用普通循环和 Vector API 计算两个数组的点积,并做了比较。

// JEP 508: Vector API (tenth incubator in JDK 25)
// Run: java --add-modules jdk.incubator.vector VectorApiDemo.java

import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

public class VectorApiDemo {

    static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

    // 用普通循环计算两个数组的点积(a[0]*b[0] + a[1]*b[1] + ...)
    static float scalarDot(float[] a, float[] b) {
        float sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    // 用 Vector API 做同样的计算,每次处理 SPECIES.length() 个元素
    static float vectorDot(float[] a, float[] b) {
        var acc = FloatVector.zero(SPECIES);
        int i = 0;
        int upper = SPECIES.loopBound(a.length);
        for (; i < upper; i += SPECIES.length()) {
            var va = FloatVector.fromArray(SPECIES, a, i);
            var vb = FloatVector.fromArray(SPECIES, b, i);
            // acc += va * vb(乘加运算)
            acc = va.fma(vb, acc);
        }
        float sum = acc.reduceLanes(VectorOperators.ADD);
        // 剩余的元素用普通(标量)方式处理
        for (; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        System.out.println("species = " + SPECIES + ", lanes per operation = " + SPECIES.length());

        float[] small = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        System.out.println("dot(small, small): scalar = " + scalarDot(small, small) + ", vector = " + vectorDot(small, small));

        // 对 100 万个元素的点积计算 500 次并比较耗时
        float check = 0;
        float[] a = new float[1_000_000];
        float[] b = new float[1_000_000];
        for (int i = 0; i < a.length; i++) {
            a[i] = (i % 100) / 100f;
            b[i] = (i % 7) / 7f;
        }
        // 计时前先让两个方法都被 JIT 编译(预热)
        float[] tiny = new float[1024];
        for (int r = 0; r < 20_000; r++) {
            check += scalarDot(tiny, tiny) + vectorDot(tiny, tiny);
        }
        for (int round = 1; round <= 3; round++) {
            long t0 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += scalarDot(a, b);
            long t1 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += vectorDot(a, b);
            long t2 = System.nanoTime();
            System.out.printf("round %d: scalar %4d ms | vector %4d ms%n", round, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000);
        }
        // 使用计算结果,防止计算被优化掉
        if (check == 42) System.out.println();
    }
}
WARNING: Using incubator modules: jdk.incubator.vector
species = Species[float, 4, S_128_BIT], lanes per operation = 4
dot(small, small): scalar = 385.0, vector = 385.0
round 1: scalar  544 ms | vector  202 ms
round 2: scalar  544 ms | vector  200 ms
round 3: scalar  577 ms | vector  197 ms

在这个环境(Apple Silicon)中,每次处理 4 个 float,比普通循环快约 2.8 倍。加入预热,是因为如果在 JIT 编译器消除 FloatVector 等对象、换成向量指令之前计时,Vector API 有时反而会显得更慢。

后续进展:由于要等待 Valhalla 项目(值对象)完成,JDK 27 中仍是第 12 次孵化(JEP 537)。

运行时・监控

本章的特性都 不用改代码,只靠 JVM 选项 就能见效。

Compact Object Headers(JEP 519)

每个 Java 对象的开头都有一个 对象头。它由保存锁、哈希码、GC 信息的“标记字(mark word)”和指向类的“类指针”组成。在 64 位 JVM 中,它占 12 字节(96 位)。JEP 519 把它压缩到了 8 字节(64 位)。

只有 2 个 int 的 record Point(int x, int y),在内存中的布局如下。

标记字类指针xy填充合计
以前8 B4 B4 B4 B4 B24 B
Compact Object Headers8 B(含类指针)―4 B4 B―16 B

对象的大小按 8 字节向上取整,所以以前对象头 12 字节 + 字段 8 字节 = 20 字节,加上填充后变成 24 字节。对象头变成 8 字节后,正好放进 16 字节。

创建 1000 万个 record Point(int x, int y),比较堆的使用量。

// JEP 519: Compact Object Headers
// Run: java -Xmx2g CompactHeadersDemo.java
// Run: java -Xmx2g -XX:+UseCompactObjectHeaders CompactHeadersDemo.java

import java.lang.management.ManagementFactory;

public class CompactHeadersDemo {

    // 只有 2 个 int 的小对象
    record Point(int x, int y) {}

    static final int COUNT = 10_000_000;

    static long usedHeap() {
        for (int i = 0; i < 3; i++) System.gc();
        Runtime rt = Runtime.getRuntime();
        return rt.totalMemory() - rt.freeMemory();
    }

    public static void main(String[] args) {
        boolean compact = ManagementFactory.getRuntimeMXBean().getInputArguments().contains("-XX:+UseCompactObjectHeaders");
        System.out.println("UseCompactObjectHeaders = " + compact);

        long before = usedHeap();
        Point[] points = new Point[COUNT];
        for (int i = 0; i < COUNT; i++) {
            points[i] = new Point(i, -i);
        }
        long after = usedHeap();

        // 减去数组本身(每个引用 4 字节 × 1000 万),求出每个 Point 的大小
        long arrayBytes = 4L * COUNT;
        double perObject = (double) (after - before - arrayBytes) / COUNT;
        System.out.printf("%,d Points: %,d MB on the heap, %.1f bytes per Point%n",
                COUNT, (after - before) / 1024 / 1024, perObject);
        System.out.println("check: " + points[COUNT - 1].y());
    }
}
java -Xmx2g CompactHeadersDemo.java
UseCompactObjectHeaders = false
10,000,000 Points: 267 MB on the heap, 24.1 bytes per Point
check: -9999999
java -Xmx2g -XX:+UseCompactObjectHeaders CompactHeadersDemo.java
UseCompactObjectHeaders = true
10,000,000 Points: 191 MB on the heap, 16.1 bytes per Point
check: -9999999
以前Compact Object Headers
每个 Point24.1 字节16.1 字节
1000 万个(含数组)267 MB191 MB(约减少 28%)

和计算一致,每个 Point 从 24 字节降到 16 字节,减少了三分之一。实际应用中会混有大对象,效果会小一些。JEP 519 报告说,在 SPECjbb2015 基准测试中,堆使用量减少了 22%,CPU 时间减少了 8%。大量处理小对象的应用(缓存、集合、事件处理等)效果最明显。

后续进展:JDK 27 默认启用(JEP 534)。想恢复以前的对象头,可以指定 -XX:-UseCompactObjectHeaders。

AOT 缓存的改进(JEP 514・JEP 515)

Java 应用启动慢的原因之一,是每次启动都要重复加载、校验、链接类。JDK 24 的 JEP 483 引入了 AOT 缓存,把这些结果保存到文件里,下次直接复用。Java 25 在两个方面改进了它。

  • JEP 514(简化命令行):JDK 24 需要运行两次,分别是记录(-XX:AOTMode=record)和创建(-XX:AOTMode=create)。Java 25 只要加上 -XX:AOTCacheOutput 运行一次 就能生成缓存。
  • JEP 515(方法性能分析):把试运行时收集的“哪些方法被频繁调用”的性能分析信息也存入缓存。正式运行时,JIT 编译器一开始就能使用这些信息,更快达到峰值性能。
flowchart LR
    A["① 试运行<br/>-XX:AOTCacheOutput=app.aot"] --> B["app.aot<br/>类 + 性能分析信息"]
    B --> C["② 正式运行<br/>-XX:AOTCache=app.aot"]

我们用一个会解析 CSV、汇总、格式化日期、构造 HTTP 请求、使用虚拟线程的小应用做了试验。AOT 缓存的类路径不能指定目录,所以先打一个 JAR 包。

// JEP 514: Ahead-of-Time Command-Line Ergonomics / JEP 515: Ahead-of-Time Method Profiling
// Run: javac -d out AotCacheDemo.java && jar --create --file app.jar -C out .
// Run: java -cp app.jar AotCacheDemo
// Run: java -XX:AOTCacheOutput=app.aot -cp app.jar AotCacheDemo
// Run: java -XX:AOTCache=app.aot -cp app.jar AotCacheDemo

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class AotCacheDemo {

    record Order(String id, LocalDate date, String category, int amount) {}

    static final String CSV = """
            A-1,2025-09-16,book,1200
            A-2,2025-09-16,food,800
            A-3,2025-09-17,book,2400
            A-4,2025-09-18,toy,1500
            """;

    public static void main(String[] args) throws Exception {
        // 一个小“应用”:解析 CSV、汇总、格式化日期、构造 HTTP 请求、虚拟线程
        Pattern comma = Pattern.compile(",");
        List<Order> orders = CSV.lines()
                .map(comma::split)
                .map(f -> new Order(f[0], LocalDate.parse(f[1]), f[2], Integer.parseInt(f[3])))
                .toList();
        Map<String, Integer> byCategory = orders.stream()
                .collect(Collectors.groupingBy(Order::category, java.util.TreeMap::new, Collectors.summingInt(Order::amount)));

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com/report"))
                .timeout(Duration.ofSeconds(5))
                .POST(HttpRequest.BodyPublishers.ofString(byCategory.toString()))
                .build();
        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build();

        int total;
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            total = executor.submit(() -> orders.stream().mapToInt(Order::amount).sum()).get();
        }

        String first = orders.getFirst().date().format(DateTimeFormatter.ofPattern("yyyy/MM/dd (EEE)", Locale.US));
        System.out.println(byCategory + " total=" + total + " first=" + first + " " + request.method()
                + " " + client.version());

        // 从 JVM 进程启动到这里所用的时间
        Instant start = ProcessHandle.current().info().startInstant().orElseThrow();
        System.out.println("finished " + Duration.between(start, Instant.now()).toMillis() + " ms after the JVM started");
    }
}
javac -d out AotCacheDemo.java && jar --create --file app.jar -C out .
java -cp app.jar AotCacheDemo
{book=3600, food=800, toy=1500} total=5900 first=2025/09/16 (Tue) POST HTTP_2
finished 245 ms after the JVM started
java -XX:AOTCacheOutput=app.aot -cp app.jar AotCacheDemo
{book=3600, food=800, toy=1500} total=5900 first=2025/09/16 (Tue) POST HTTP_2
finished 338 ms after the JVM started
[0.377s][warning][aot] Skipping jdk/internal/event/Event: Not in loaded state
[0.377s][warning][aot] Skipping jdk/internal/event/Event: JFR event class
... (8 more [warning][aot] lines about JFR event classes)
Temporary AOTConfiguration recorded: app.aot.config
Launching child process /path/to/jdk-25/bin/java to assemble AOT cache app.aot using configuration app.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=app.jar -XX:AOTCacheOutput=app.aot -XX:AOTConfiguration=app.aot.config -XX:AOTMode=create
Reading AOTConfiguration app.aot.config and writing AOTCache app.aot
AOTCache creation is complete: app.aot 22265856 bytes
Removed temporary AOT configuration file app.aot.config
java -XX:AOTCache=app.aot -cp app.jar AotCacheDemo
{book=3600, food=800, toy=1500} total=5900 first=2025/09/16 (Tue) POST HTTP_2
finished 202 ms after the JVM started

试运行之后,生成了约 22 MB 的缓存文件 app.aot。接着,不用缓存和使用缓存各交替运行 15 次,比较从 JVM 启动到处理完成的时间。

从启动到完成的时间(实测,15 次的中位数,刻度单位:s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 100millisecond
    section 无缓存
    279 ms :crit, 0, 279
    section 使用 AOT 缓存
    169 ms :active, 0, 169

使用缓存后,从启动到完成的时间 缩短了约 4 成,而代码一行都没改。像 Spring Boot 这样启动时加载大量类的应用,效果会更明显。

缓存只能在与创建时 相同的 JDK、相同的类路径 下使用。替换了 JAR 就需要重新生成。条件不符时,JVM 会输出警告并在不使用缓存的情况下启动,不会导致应用无法运行。

后续进展:JDK 26 的 JEP 516 让包括 ZGC 在内的 任何 GC 都能缓存对象(之前只限部分 GC)。Project Leyden 仍在继续缩短启动和预热时间。

Generational Shenandoah(JEP 521)

Shenandoah 是一种能让应用停顿时间(暂停时间)保持极短的 GC。JDK 24 中以实验性方式引入的 分代模式,在 Java 25 成为正式特性。这和 Java 21 中 ZGC 变为分代是同样的思路。

它基于 “大多数对象很快就会死亡” 这一经验法则(分代假说),把堆分成年轻代和老年代,频繁回收年轻代。在非分代模式下,每次 GC 都要遍历 包括长寿对象在内的整个堆。

为了让老年代的差异更明显,先创建约 600 MB 一直存活到最后的数据,再创建 3000 万个 1 KB 的短命对象。

// JEP 521: Generational Shenandoah
// Run: java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xmx2g GenerationalShenandoahDemo.java
// Run: java -XX:+UseShenandoahGC -Xmx2g GenerationalShenandoahDemo.java

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;

public class GenerationalShenandoahDemo {

    public static void main(String[] args) {
        // 只保留最近的 5 万个对象(很快就会变成垃圾的短命对象)
        byte[][] recent = new byte[50_000][];
        // 先创建约 600 MB 一直存活到最后的数据(假装是缓存之类)
        List<byte[]> longLived = new ArrayList<>();
        for (int i = 0; i < 600_000; i++) {
            longLived.add(new byte[1024]);
        }
        long start = System.nanoTime();

        for (int i = 0; i < 30_000_000; i++) {
            recent[i % recent.length] = new byte[1024];
        }

        System.out.printf("allocated ~%d GB, long-lived %,d, elapsed %,d ms%n",
                30_000_000L * 1024 / 1024 / 1024 / 1024, longLived.size(), (System.nanoTime() - start) / 1_000_000);
        for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
            System.out.printf("  %-20s count %4d, total %,6d ms%n", gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
        }
    }
}
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xmx2g GenerationalShenandoahDemo.java
allocated ~28 GB, long-lived 600,000, elapsed 4,842 ms
  Shenandoah Pauses    count   76, total     14 ms
  Shenandoah Cycles    count   37, total  1,073 ms
java -XX:+UseShenandoahGC -Xmx2g GenerationalShenandoahDemo.java
allocated ~28 GB, long-lived 600,000, elapsed 8,978 ms
  Shenandoah Pauses    count  114, total     19 ms
  Shenandoah Cycles    count   56, total  4,445 ms

两种模式各运行 5 次,中位数如下。

耗时与 GC 周期合计时间,5 次的中位数(实测,刻度单位:s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 2second
    section 分代 耗时
    4842 ms :active, 0, 4842
    section 分代 GC
    1072 ms :active, 0, 1072
    section 非分代 耗时
    8978 ms :crit, 0, 8978
    section 非分代 GC
    4491 ms :crit, 0, 4491

在分代模式下,GC 并发运行的时间(Cycles)合计 减少到约四分之一,整个处理的耗时也 几乎减半。这是因为不用每次都遍历那 600 MB 长寿数据。暂停时间(Pauses)在两种模式下合计都只有 20 ms 左右或更少,非常短。

测量值的波动很大。即使条件相同,分代模式的耗时也在 4.5〜6.0 s 之间,非分代模式在 8.3〜13.1 s 之间。另外,在长寿数据较少的负载下(最初的尝试中约 60 MB),两种模式几乎没有差别。要不要用分代模式,最好在自己的应用上测量后再决定。

JFR(JDK Flight Recorder)

Java 25 中,JFR 加入了 3 个 JEP。JFR 是内置于 JVM 的低开销分析器兼事件记录功能。

JFR Method Timing & Tracing(JEP 520)

现在可以 不改代码,就测量指定方法的调用次数和执行时间了。JVM 会自动在字节码中插入测量处理。

// JEP 520: JFR Method Timing & Tracing
// Run: java '-XX:StartFlightRecording:method-timing=MethodTimingDemo::parse;java.lang.Integer::parseInt,filename=timing.jfr' MethodTimingDemo.java
// Run: jfr view method-timing timing.jfr

import java.util.ArrayList;
import java.util.List;

public class MethodTimingDemo {

    // 想要测量的方法。代码里没有写任何测量用的处理
    static List<Integer> parse(String csv) {
        List<Integer> values = new ArrayList<>();
        for (String s : csv.split(",")) {
            values.add(Integer.parseInt(s.trim()));
        }
        return values;
    }

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1_000; i++) {
            sb.append(i).append(", ");
        }
        sb.append(1_000);
        String csv = sb.toString();

        long sum = 0;
        for (int i = 0; i < 2_000; i++) {
            sum += parse(csv).size();
        }
        System.out.println("parsed " + sum + " values");
    }
}
java '-XX:StartFlightRecording:method-timing=MethodTimingDemo::parse;java.lang.Integer::parseInt,filename=timing.jfr' MethodTimingDemo.java
[0.373s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.373s][info][jfr,startup] 
[0.373s][info][jfr,startup] Use jcmd 42053 JFR.dump name=1 to copy recording data to file.
parsed 2002000 values
jfr view method-timing timing.jfr

                                               Method Timing

Timed Method                                            Invocations Minimum Time Average Time Maximum Time
------------------------------------------------------- ----------- ------------ ------------ ------------
MethodTimingDemo.parse(String)                                2,000  0.078100 ms  0.124000 ms  5.410000 ms
java.lang.Integer.parseInt(CharSequence, int, int, int)           3  0.000334 ms  0.002850 ms  0.006960 ms
java.lang.Integer.parseInt(String)                        2,002,001  0.000041 ms  0.000061 ms  0.098300 ms
java.lang.Integer.parseInt(String, int)                   2,002,007  0.000019 ms  0.000019 ms  0.057200 ms

不仅是自己写的 parse,JDK 的方法(Integer.parseInt)也能同样测量。指定方法时,除了 类名::方法名,还可以只写类名(该类的所有方法),或者使用 @jakarta.ws.rs.GET 这样的注解。指定 method-trace 的话,还会记录每次调用的堆栈。

以前需要加日志重新部署,或者使用 Java agent。要在生产环境中确认“这个方法好像很慢”,这是一个很有力的工具。不过,插入了测量处理的方法会变慢。在这个示例中,把 Integer.parseInt 也加入测量对象后,parse 的平均时间从约 0.06 ms 增加到了约 0.12 ms。测量对象要尽量收窄。

JFR CPU-Time Profiling(JEP 509・实验性)

以往 JFR 的采样方式是“每隔一定时间,抓取正在运行的线程的堆栈”,无法准确捕捉执行本地代码时的情况。JEP 509 利用 Linux 内核的 CPU 定时器,按照 实际使用 CPU 的时间 成比例地采样。这是仅限 Linux 的实验性功能,所以没有在本文的 macOS 环境中尝试。在 Linux 上这样使用:

java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr App.java
jfr view cpu-time-hot-methods profile.jfr

JFR Cooperative Sampling(JEP 518)

重新设计了 JFR 对堆栈采样的机制:当线程到达安全点(safepoint)时,由线程自己记录堆栈。采样的稳定性和精度都提高了。这是 JVM 内部的改进,使用者不需要做任何事。

删除 32 位 x86 移植(JEP 503)

面向 32 位 x86 的源代码和构建机制从 JDK 中删除了。它在 JDK 24 被弃用(JEP 501)。Windows 版已在 JDK 24 删除(JEP 479),至此 32 位 x86 的支持完全结束。这样就不必再为 32 位实现虚拟线程等新功能了。

总结:Java 25 项目中用什么

最后,从在现有项目中应用的角度做个整理。

分类特性判断
马上能用Compact Source Files、Module Import、Flexible Constructor、Scoped Values、KDF API正式特性。从新写的代码开始使用
试试选项Compact Object Headers、AOT 缓存、分代 Shenandoah不改代码就能见效。测量后决定是否采用
用于监控JFR Method Timing & Tracing在生产环境排查时,用来确认“这个方法很慢”
观望Structured Concurrency、Stable Values、PEM、Primitive Patterns、Vector API预览或孵化。API 还会变(Stable Values 已经改名)

从 Java 21 迁移时,特别值得关注的是运行时的 3 项。对象头压缩让堆减少了三分之一,AOT 缓存让启动快了约 4 成。这两项效果都是在 不改应用一行代码 的情况下得到的。不妨先从修改 JVM 选项并测量开始。

参考资料

© 2020 - 2026 issyuu @Lakeheart Retreat
Powered by theme astro-koharu · Inspired by Shoka
加载中...