前言
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) |
|---|---|---|---|
| 470 | PEM Encodings of Cryptographic Objects | 预览 | JDK 27 仍是第 3 次预览 |
| 502 | Stable Values | 预览 | 改名为 Lazy Constants,JDK 27 为第 3 次预览 |
| 503 | 删除 32 位 x86 移植 | 删除 | ― |
| 505 | Structured Concurrency | 第 5 次预览 | JDK 27 仍是第 7 次预览 |
| 506 | Scoped Values | 正式 | ― |
| 507 | Primitive Types in Patterns | 第 3 次预览 | JDK 27 仍是第 5 次预览 |
| 508 | Vector API | 第 10 次孵化 | JDK 27 仍是第 12 次孵化 |
| 509 | JFR CPU-Time Profiling | 实验性(仅 Linux) | ― |
| 510 | Key Derivation Function API | 正式 | ― |
| 511 | Module Import Declarations | 正式 | ― |
| 512 | Compact Source Files and Instance Main Methods | 正式 | ― |
| 513 | Flexible Constructor Bodies | 正式 | ― |
| 514 | 简化 AOT 命令行 | 正式 | JDK 26 起任何 GC 都能缓存对象 |
| 515 | AOT 方法性能分析 | 正式 | 同上 |
| 518 | JFR Cooperative Sampling | 正式 | ― |
| 519 | Compact Object Headers | 正式(可选) | JDK 27 默认启用 |
| 520 | JFR Method Timing & Tracing | 正式 | ― |
| 521 | Generational 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 个。
- 隐式类:写在顶层的字段和方法,会成为编译器自动生成的类(类名与文件名相同)的成员。
- 实例
main:不再需要static、public和参数。如果main是实例方法,JVM 会先创建实例再调用它。 - 自动 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 = 1410065408value 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 的区别如下。
| ThreadLocal | ScopedValue | |
|---|---|---|
| 修改值 | 随时可以用 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 = truePEM 是交换密钥和证书时最常用的格式,但以前 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),在内存中的布局如下。
| 标记字 | 类指针 | x | y | 填充 | 合计 | |
|---|---|---|---|---|---|---|
| 以前 | 8 B | 4 B | 4 B | 4 B | 4 B | 24 B |
| Compact Object Headers | 8 B(含类指针) | ― | 4 B | 4 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 | |
|---|---|---|
| 每个 Point | 24.1 字节 | 16.1 字节 |
| 1000 万个(含数组) | 267 MB | 191 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.jfrJFR 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 选项并测量开始。
参考资料
- JDK 25 项目页面
- JEP 512: Compact Source Files and Instance Main Methods
- JEP 511: Module Import Declarations
- JEP 513: Flexible Constructor Bodies
- JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
- JEP 506: Scoped Values
- JEP 505: Structured Concurrency (Fifth Preview) / JEP 525: Structured Concurrency (Sixth Preview)
- JEP 502: Stable Values (Preview) / JEP 526: Lazy Constants (Second Preview)
- JEP 510: Key Derivation Function API
- JEP 470: PEM Encodings of Cryptographic Objects (Preview)
- JEP 519: Compact Object Headers / JEP 534: Compact Object Headers by Default
- JEP 514: Ahead-of-Time Command-Line Ergonomics / JEP 515: Ahead-of-Time Method Profiling
- JEP 521: Generational Shenandoah
- JEP 520: JFR Method Timing & Tracing / JEP 509: JFR CPU-Time Profiling (Experimental)
- JDK 27 项目页面