JDK 24 新特性解析
Categories: 编程
前言
JDK 24 是 Java SE 平台版本 24 的参考实现,预计 2025 年 3 月正式发布。本文将详细解析 JDK 24 的主要新特性。
新特性总览
| JEP | 特性名称 | 状态 | 说明 |
|---|---|---|---|
| 467 | Markdown 文档注释 | 正式发布 | 允许在 JavaDoc 中使用 Markdown 语法,提高文档可读性 |
| 469 | 向量 API | 第九次孵化 | 提供向量计算 API,支持 SIMD 指令优化 |
| 471 | 弃用 sun.misc.Unsafe 中的内存访问方法 |
正式发布 | 继续推进弃用不安全的内存访问方法 |
| 474 | ZGC:默认的分代模式 | 正式发布 | 分代 ZGC 成为默认模式,提升垃圾回收性能 |
| 476 | 模块导入声明 | 预览 | 简化模块导入,允许一次性导入整个模块 |
| 477 | 隐式声明的类和实例主方法 | 第三次预览 | 简化初学者编程体验 |
| 480 | 结构化并发 | 第三次预览 | 简化并发编程模型 |
| 481 | 作用域值 | 第三次预览 | 提供线程间共享不可变数据的新机制 |
| 482 | 灵活的构造函数体 | 第二次预览 | 允许在 super() 调用前执行语句 |
| 483 | 提前编译工具改进 | 正式发布 | 改进 AOT 编译工具,提升启动性能 |
详细解析
JEP 467:Markdown 文档注释(正式发布)
JEP 467 在经过预览后正式发布,允许在 JavaDoc 文档注释中使用 Markdown 语法。
为什么要引入这个功能:
传统的 JavaDoc 主要使用 HTML 标签进行标记,语法繁琐且容易出错。Markdown 作为一种轻量级标记语言,具有简洁、易读、易写的特点,已在开发者社区广泛使用。引入 Markdown 支持可以显著简化文档编写过程,提高代码的可读性和可维护性。
目标:
- 简化文档编写:使用简洁的 Markdown 语法替代繁琐的 HTML
- 提高可读性:Markdown 文档在源代码中也易于阅读
- 保持兼容性:继续支持传统的 HTML 和 JavaDoc 标签
- 降低学习成本:开发者无需深入学习 HTML 即可编写文档
简要使用示例:
- 使用 Markdown 语法的文档注释:
/**
* 用户服务类,提供用户管理的核心功能。
*
* ## 主要功能
*
* - 用户注册和登录
* - 用户信息管理
* - 权限验证
*
* ### 使用示例
*
* ```java
* UserService service = new UserService();
* User user = service.createUser("john", "john@example.com");
* ```
*
* **注意事项:**
*
* 1. 用户名必须唯一
* 2. 邮箱格式必须有效
* 3. 密码长度至少 8 位
*
* @param username 用户名
* @param email 邮箱地址
* @return 创建的用户对象
* @throws IllegalArgumentException 如果参数无效
*/
public class UserService {
// ...
}
- Markdown 表格支持:
/**
* 订单状态枚举
*
* | 状态 | 描述 | 可执行操作 |
* |------|------|-----------|
* | PENDING | 待处理 | 确认、取消 |
* | CONFIRMED | 已确认 | 发货、取消 |
* | SHIPPED | 已发货 | 签收 |
* | COMPLETED | 已完成 | 评价、退货 |
*/
public enum OrderStatus {
PENDING, CONFIRMED, SHIPPED, COMPLETED
}
JEP 469:向量 API(第九次孵化)
JEP 469(第九次孵化)继续改进向量 API,提供更高效的向量计算能力。
为什么要引入这个功能:
现代 CPU 普遍支持 SIMD(Single Instruction, Multiple Data)指令,可以在单条指令中处理多个数据,显著提升计算性能。然而 Java 缺乏直接利用这些硬件能力的标准 API。向量 API 填补了这一空白,使开发者能够编写高性能的数值计算代码。
目标:
- 跨平台可移植性:编写一次,在不同 CPU 架构上都能高效运行
- 可预测的性能:向量操作可靠地编译为高效的机器指令
- 优雅降级:在不支持向量指令的平台上自动降级为标量操作
- 易于使用:提供清晰简洁的 API
简要使用示例:
- 向量化数组加法:
import jdk.incubator.vector.*;
public class VectorExample {
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
public static void vectorAdd(float[] a, float[] b, float[] result) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
// 向量化循环
for (; i < upperBound; i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.add(vb);
vc.intoArray(result, i);
}
// 处理剩余元素
for (; i < a.length; i++) {
result[i] = a[i] + b[i];
}
}
}
- 向量化点积计算:
public static float dotProduct(float[] a, float[] b) {
var sum = FloatVector.zero(SPECIES);
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
sum = va.fma(vb, sum); // Fused multiply-add
}
float result = sum.reduceLanes(VectorOperators.ADD);
// 处理剩余元素
for (; i < a.length; i++) {
result += a[i] * b[i];
}
return result;
}
JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法(正式发布)
JEP 471 正式将 sun.misc.Unsafe 类中的内存访问方法标记为弃用,为未来移除做准备。
为什么要引入这个功能:
sun.misc.Unsafe 是一个非标准的内部 API,其内存访问方法可能导致未定义行为甚至 JVM 崩溃。随着 VarHandle API(JDK 9)和 Foreign Function & Memory API(JDK 22)的引入,已经有了安全且高效的替代方案。
目标:
- 促进迁移:鼓励开发者使用标准 API
- 提高安全性:减少不安全内存操作导致的问题
- 为移除做准备:在未来版本中完全移除这些方法
简要使用示例:
- 使用 VarHandle 替代 Unsafe:
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class SafeMemoryAccess {
private volatile int value;
private static final VarHandle VALUE_HANDLE;
static {
try {
VALUE_HANDLE = MethodHandles.lookup()
.findVarHandle(SafeMemoryAccess.class, "value", int.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
// 原子性读取
public int getValue() {
return (int) VALUE_HANDLE.getVolatile(this);
}
// 原子性写入
public void setValue(int newValue) {
VALUE_HANDLE.setVolatile(this, newValue);
}
// CAS 操作
public boolean compareAndSet(int expected, int newValue) {
return VALUE_HANDLE.compareAndSet(this, expected, newValue);
}
}
- 使用 Foreign Memory API:
import java.lang.foreign.*;
public class OffHeapMemory {
public static void allocateAndUseMemory() {
try (Arena arena = Arena.ofConfined()) {
// 分配堆外内存
MemorySegment segment = arena.allocate(1024);
// 写入数据
segment.set(ValueLayout.JAVA_INT, 0, 42);
segment.set(ValueLayout.JAVA_INT, 4, 100);
// 读取数据
int value1 = segment.get(ValueLayout.JAVA_INT, 0);
int value2 = segment.get(ValueLayout.JAVA_INT, 4);
System.out.println("Values: " + value1 + ", " + value2);
} // 内存自动释放
}
}
JEP 474:ZGC:默认的分代模式(正式发布)
JEP 474 正式将 Z 垃圾收集器(ZGC)的默认模式切换为分代模式。
为什么要引入这个功能:
分代 ZGC 在大多数使用场景下性能优于非分代模式,能够更好地处理不同生命周期的对象。维护两种模式增加了开发和测试成本,统一为分代模式可以集中资源进行优化。
目标:
- 提升默认性能:大多数应用无需调优即可获得更好的性能
- 简化维护:专注于分代 ZGC 的发展
- 降低内存占用:分代模式通常使用更少的内存
简要使用示例:
- 启用 ZGC(分代模式,JDK 24 默认):
# JDK 24 中,直接启用 ZGC 即使用分代模式
java -XX:+UseZGC -Xmx4g MyApplication
# 查看 GC 日志
java -XX:+UseZGC -Xlog:gc* -Xmx4g MyApplication
- 如果需要使用非分代模式(已弃用):
# 显式禁用分代模式(会收到弃用警告)
java -XX:+UseZGC -XX:-ZGenerational -Xmx4g MyApplication
# 警告信息:
# Warning: Option ZGenerational was deprecated.
# Warning: Non-generational mode is deprecated and will likely be removed in a future release.
- ZGC 调优参数:
# 设置并发 GC 线程数
java -XX:+UseZGC -XX:ConcGCThreads=2 MyApplication
# 设置最大堆大小
java -XX:+UseZGC -Xmx8g MyApplication
# 启用详细的 GC 日志
java -XX:+UseZGC -Xlog:gc*=info:file=gc.log MyApplication
JEP 476:模块导入声明(预览)
JEP 476 引入模块导入声明,简化了模块化库的使用。
为什么要引入这个功能:
使用模块化库时,开发者通常需要导入多个包。模块导入声明允许一次性导入整个模块的所有导出包,减少样板代码,提高开发效率。
目标:
- 简化代码:用一条语句替代多条包导入
- 降低学习曲线:无需了解模块内部的包结构
- 提高可维护性:模块添加新包时无需修改导入语句
简要使用示例:
// 传统方式:需要导入多个包
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
// 使用模块导入:一条语句导入整个模块
import module java.sql;
public class DatabaseExample {
public void queryDatabase() throws SQLException {
// 可以直接使用 java.sql 模块的所有导出类型
Connection conn = DriverManager.getConnection("jdbc:...");
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
}
JEP 477:隐式声明的类和实例主方法(第三次预览)
JEP 477(第三次预览)继续简化 Java 程序的编写,特别适合初学者和小型程序。
为什么要引入这个功能:
传统的 Java 程序需要理解类、访问修饰符、静态方法、数组参数等多个概念,这对初学者构成了不必要的障碍。通过允许省略这些样板代码,降低了学习门槛。
目标:
- 降低入门门槛:使初学者能够快速上手
- 简化脚本编写:减少工具脚本的冗余代码
- 平滑学习曲线:随着需求增长逐步引入高级特性
简要使用示例:
- 最简单的程序:
// HelloWorld.java
void main() {
println("Hello, World!");
}
- 使用实例字段和方法:
// Calculator.java
int accumulator = 0;
void main() {
add(10);
add(20);
println("Total: " + accumulator);
}
void add(int value) {
accumulator += value;
}
- 逐步扩展到完整的类:
// 初学者版本
void main() {
println("Hello");
}
// 进阶版本 - 添加字段和方法
String name = "Java";
void main() {
greet();
}
void greet() {
println("Hello, " + name);
}
// 最终版本 - 标准类结构
public class Greeter {
private String name = "Java";
public static void main(String[] args) {
new Greeter().greet();
}
public void greet() {
System.out.println("Hello, " + name);
}
}
JEP 480:结构化并发(第三次预览)
JEP 480(第三次预览)继续完善结构化并发 API,简化并发编程。
为什么要引入这个功能:
传统的并发编程模型容易出现线程泄漏、异常处理复杂、取消操作困难等问题。结构化并发通过将相关任务作为单个工作单元管理,提供了更清晰、更安全的并发编程模式。
目标:
- 简化并发代码:提供清晰的任务生命周期管理
- 提高可靠性:自动处理任务取消和资源清理
- 增强可观察性:更容易监控和调试并发任务
简要使用示例:
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class UserDataService {
record UserData(User user, List<Order> orders, Profile profile) {}
/**
* 并发获取用户的所有相关数据
*/
public UserData fetchUserData(String userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 并发执行多个任务
Subtask<User> userTask = scope.fork(() -> fetchUser(userId));
Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(userId));
Subtask<Profile> profileTask = scope.fork(() -> fetchProfile(userId));
// 等待所有任务完成
scope.join();
// 如果任何任务失败,抛出异常
scope.throwIfFailed();
// 所有任务成功,组装结果
return new UserData(
userTask.get(),
ordersTask.get(),
profileTask.get()
);
}
}
/**
* 获取第一个成功响应的结果
*/
public String fetchFromMultipleServers(List<String> serverUrls) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
// 并发请求多个服务器
for (String url : serverUrls) {
scope.fork(() -> httpGet(url));
}
// 等待第一个成功的结果
scope.join();
// 返回第一个成功的结果
return scope.result();
}
}
private User fetchUser(String userId) {
// 模拟数据库查询
return new User(userId, "John Doe");
}
private List<Order> fetchOrders(String userId) {
// 模拟订单查询
return List.of(new Order("Order1"), new Order("Order2"));
}
private Profile fetchProfile(String userId) {
// 模拟用户档案查询
return new Profile(userId, "Premium");
}
private String httpGet(String url) {
// 模拟 HTTP 请求
return "Response from " + url;
}
}
JEP 481:作用域值(第三次预览)
JEP 481(第三次预览)提供了一种新的方式在线程间共享不可变数据,作为 ThreadLocal 的更好替代方案。
为什么要引入这个功能:
ThreadLocal 存在多个问题:可变性导致的线程安全隐患、不受控制的生命周期、在虚拟线程场景下的性能问题等。作用域值通过提供不可变的、生命周期明确的数据共享机制,解决了这些问题。
目标:
- 不可变性:确保共享数据的安全性
- 明确的生命周期:从代码结构中清晰可见
- 高性能:特别是在虚拟线程场景下
简要使用示例:
import jdk.incubator.concurrent.ScopedValue;
public class WebRequestHandler {
// 定义作用域值
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
private static final ScopedValue<Locale> LOCALE = ScopedValue.newInstance();
/**
* 处理 HTTP 请求
*/
public void handleRequest(HttpRequest request) {
String requestId = generateRequestId();
User user = authenticate(request);
Locale locale = determineLocale(request);
// 设置作用域值并执行请求处理
ScopedValue.where(REQUEST_ID, requestId)
.where(CURRENT_USER, user)
.where(LOCALE, locale)
.run(() -> processRequest(request));
}
private void processRequest(HttpRequest request) {
// 在作用域内可以访问值
log("Processing request");
// 调用业务逻辑
var data = fetchData();
var response = transformData(data);
sendResponse(response);
log("Request completed");
}
private Object fetchData() {
// 可以在任何嵌套的方法中访问作用域值
User user = CURRENT_USER.get();
return database.query("SELECT * FROM data WHERE user_id = ?", user.id());
}
private Object transformData(Object data) {
Locale locale = LOCALE.get();
// 根据语言环境转换数据
return localizer.transform(data, locale);
}
private void sendResponse(Object response) {
String requestId = REQUEST_ID.get();
// 发送响应
System.out.println("Sending response for request: " + requestId);
}
private void log(String message) {
String requestId = REQUEST_ID.get();
User user = CURRENT_USER.get();
System.out.printf("[%s] [User: %s] %s%n", requestId, user.name(), message);
}
private String generateRequestId() {
return java.util.UUID.randomUUID().toString();
}
private User authenticate(HttpRequest request) {
return new User("user123", "John Doe");
}
private Locale determineLocale(HttpRequest request) {
return Locale.US;
}
}
JEP 482:灵活的构造函数体(第二次预览)
JEP 482(第二次预览)继续完善灵活的构造函数体功能,根据第一次预览的反馈进行了改进。
为什么要引入这个功能:
Java 要求构造函数的第一条语句必须是 super() 或 this() 调用,这限制了参数验证和数据准备的灵活性。新特性允许在调用父类构造函数前执行必要的准备工作。
目标:
- 增强灵活性:允许在
super()前进行参数验证和准备 - 减少辅助方法:无需创建静态方法来准备参数
- 提高代码质量:使构造函数逻辑更清晰
简要使用示例:
- 参数验证:
public class Email {
private final String address;
public Email(String address) {
// 在 super() 前验证参数
if (address == null || address.isBlank()) {
throw new IllegalArgumentException("Email address cannot be empty");
}
if (!address.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
// 标准化处理
address = address.trim().toLowerCase();
super();
this.address = address;
}
}
- 复杂参数准备:
public class SecureConnection extends Connection {
public SecureConnection(Certificate certificate, int port) {
// 验证证书
if (certificate == null) {
throw new IllegalArgumentException("Certificate is required");
}
if (certificate.isExpired()) {
throw new IllegalArgumentException("Certificate has expired");
}
// 提取和处理证书信息
var publicKey = certificate.getPublicKey();
byte[] keyBytes = switch (publicKey) {
case RSAPublicKey rsa -> encodeRSAKey(rsa);
case ECPublicKey ec -> encodeECKey(ec);
default -> throw new IllegalArgumentException("Unsupported key type");
};
// 调用父类构造函数
super(keyBytes, port);
}
private byte[] encodeRSAKey(RSAPublicKey key) {
// RSA 密钥编码逻辑
return key.getEncoded();
}
private byte[] encodeECKey(ECPublicKey key) {
// EC 密钥编码逻辑
return key.getEncoded();
}
}
JEP 483:提前编译工具改进(正式发布)
JEP 483 改进了提前编译(AOT)工具,进一步提升 Java 应用的启动性能。
为什么要引入这个功能:
在云原生和 serverless 环境中,应用的启动时间至关重要。传统的 JIT(即时编译)需要在运行时编译热点代码,导致启动阶段性能较差。AOT 编译可以在构建时完成编译,显著减少启动时间。
目标:
- 加快启动速度:预编译代码可立即执行
- 减少预热时间:避免 JIT 编译导致的性能波动
- 降低资源消耗:减少运行时的 CPU 使用
简要使用示例:
- 使用 AOT 编译应用:
# 分析应用运行时行为
java -XX:ArchiveClassesAtExit=app.jsa -cp app.jar com.example.Main
# 使用存档文件启动应用(更快的启动速度)
java -XX:SharedArchiveFile=app.jsa -cp app.jar com.example.Main
- 结合 CDS(Class Data Sharing):
# 创建应用的类数据共享存档
java -Xshare:dump -XX:SharedClassListFile=classes.lst \
-XX:SharedArchiveFile=app.jsa -cp app.jar
# 使用共享存档启动
java -Xshare:on -XX:SharedArchiveFile=app.jsa \
-cp app.jar com.example.Main
- 性能对比:
# 传统启动
$ time java -cp app.jar com.example.Main
real 0m2.345s
# AOT + CDS 启动
$ time java -Xshare:on -XX:SharedArchiveFile=app.jsa \
-cp app.jar com.example.Main
real 0m0.856s
发布计划
- 2024/12/05: Rampdown Phase One
- 2025/01/16: Rampdown Phase Two
- 2025/02/06: Initial Release Candidate
- 2025/02/20: Final Release Candidate
- 2025/03/18: General Availability
总结
JDK 24 延续了 Java 平台的持续演进,在简化开发、提升性能和增强安全性方面取得了重要进展。
正式发布的特性包括 Markdown 文档注释、弃用 sun.misc.Unsafe、分代 ZGC 默认模式和 AOT 工具改进,这些特性已经成熟稳定,可以在生产环境中放心使用。
持续预览的特性如模块导入声明、隐式声明的类、结构化并发、作用域值和灵活的构造函数体,继续收集社区反馈,为最终正式发布做准备。
向量 API 进入第九次孵化,表明 Java 在高性能计算领域持续发力,为科学计算、机器学习等场景提供更好的支持。
总体而言,JDK 24 在保持企业级应用稳定性的同时,也积极拥抱现代编程实践,使 Java 更加易学易用、高效安全。
公众号
coding 笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,大家关注 ^_^
我的博客地址:博客主页。
