Ohhnews

分类导航

$ cd ..
Baeldung原文

Java 27 新特性解读:九项 JEP 落地,四项正式转正

#java 27#jdk#垃圾回收#后量子加密#预览特性

[LOADING...]

1. 概述

Oracle 发布了 Java 平台第 27 版。

其前身 Java 26 为 HTTP Client API 带来了 HTTP/3,并移除了 Applet API。Java 27 包含九个 JEP:其中四个是正式特性,五个仍处于预览或孵化阶段。 没有任何特性从预览转为正式,而四个正式 JEP 都是我们在运行时而非代码中能察觉到的。

在本教程中,我们将了解哪些变化无需调整任何代码,以及预览特性中又有哪些改动。

2. JVM 运行时中的新默认值

以下变更不需要调整代码或启动标志。

2.1. 默认启用紧凑对象头

Java 25 将 紧凑对象头(JEP 519) 作为完整特性引入,但默认禁用。后续测试和报告表明,它可以减少内存占用,并支持更快的处理和垃圾回收。 因此,该特性在 Java 27 中现已默认启用。我们仍可以通过 -XX:-UseCompactObjectHeaders 禁用它,但旧的对象布局计划被弃用,所以这只是一种临时变通方案。

默认启用紧凑对象头对使用大量小对象的应用程序影响最大,因为对象头是每个对象的固定开销。对象越小,它所占的比例就越大。围绕 DTO 和实体构建的 REST 服务就是典型例子。

请注意,如果我们之前通过测量来设置 -Xmx 或容器内存限制,那么这些值现在过于宽松了。更小的对象头意味着相同工作负载保持的存活集更小。不会有任何东西崩溃,但重新测量可以让我们降低限制并缩小容器。这会转化为实际的成本节省。

2.2. G1 成为所有环境下的默认垃圾回收器

自 Java 9 以来,G1 一直是默认垃圾回收器,但在单 CPU 或内存小于 1792 MB 的机器上除外,此时 JVM 会使用 Serial GC。Java 27 移除了这一例外,延续了 Java 26(JEP 522)中对 G1 的改进。

在实践中,这会影响小型容器:CI 运行器、sidecar 和无服务器环境。

3. 安全增强

四个正式 JEP 中有两个改进了安全性,而且两者都无需我们修改任何代码即可生效。

3.1. TLS 1.3 的后量子混合密钥交换

Java 27 在 TLS 1.3 中将经典 ECDHE 密钥交换与抗量子的 ML-KEM 相结合。使用 javax.net.ssl 的应用程序会在对端支持时自动协商这种混合方案。

该增强针对的是那些今天记录加密流量,并计划等待量子计算机有能力解密时再动手的攻击者。

将两种方案结合意味着,攻破其中一种并不足以让攻击成功。不过,ML-KEM 比 ECDHE 新得多,经历的密码分析也少得多,因此它是与经典交换并列添加,而不是取代它。即使它被证明很弱,连接也不会比今天更差。

要查看连接实际协商了哪种方案,我们可以打开 JSSE 握手日志:

$ bash
java -Djavax.net.debug=ssl,handshake -jar app.jar

在记录的日志消息中,命名组就是双方商定的密钥交换。Java 27 支持三种混合组:

  • X25519MLKEM768
  • SecP256r1MLKEM768
  • SecP384r1MLKEM1024

像 x25519 这样的普通值表示对端不支持其中任何一种。

3.2. JFR 进程内数据脱敏

Java 27 会在 JVM 内部对 JFR 记录中的敏感值进行遮蔽,在它们到达记录文件之前就完成处理。以前会出现在记录中的值,现在会显示为 [REDACTED]。

这一点很重要,因为记录会捕获命令行参数、环境变量和系统属性,而密码和令牌通常就存放在这些地方。 过去把一份记录附到支持工单上,就意味着把这些秘密也一并发送出去。

我们应当注意,脱敏是基于名称的。JVM 会将属性、变量和参数名称与一组 glob 模式进行匹配。默认设置覆盖了诸如 *password* 或 *token* 这样的名称,但我们也可以通过 JVM 标志添加自己的模式:

$ bash
java -XX:FlightRecorderOptions:redact-key=*credential*,*secret* -jar app.jar

在这里,redact-key 适用于系统属性和环境变量的名称。 使用上面的模式,db.credential 和 client.secret 的值也会在记录中被替换,而像 auth.key 这样的名称仍会原样通过。命令行参数由单独的 redact-argument 选项处理。

4. 仍处于预览阶段

有五个特性仍处于预览或孵化阶段。其中大多数都只是带着细微改进重新提交,而有一个特性包含会破坏现有代码的变更。

4.1. 结构化并发

与第六个预览版相比,这个预览版带来了破坏性代码变更:

  • joiner 现在抛出 ExecutionException,而不是 FailedException
  • Joiner 新增了第三个类型参数,用于异常类型
  • awaitAll() 被移除
  • onTimeout() 变为 timeout()

因异常类型导致的破坏在实践中可能最常见,例如:

$ java
try (var scope = StructuredTaskScope.open()) {
    Subtask<User> user = scope.fork(() -> loadUser(id));
    Subtask<List<Order>> orders = scope.fork(() -> loadOrders(id));
    scope.join();
    return new Profile(user.get(), orders.get());
} catch (ExecutionException e) {
    throw new ProfileException(e.getCause());
}

针对第六个预览版编写的每个 catch 子句都必须更新,而 ExecutionException 是受检异常,因此编译器会把这些地方指出来。

4.2. 惰性常量

惰性常量保存一个在首次访问时计算、之后行为如同常量的值。 JVM 可以像优化 final 字段那样优化它们,而 synchronized 块背后的普通惰性初始化无法做到这一点。典型用法是我们不愿在启动期间构建的昂贵对象。例如:

$ java
private static final LazyConstant<ExpensiveParser> PARSER = LazyConstant.of(ExpensiveParser::new);

第三个预览版移除了 isInitialized() 和 orElse(),这两个低层方法会诱使人们检查初始化状态,而这正是该抽象想要隐藏的内容。

作为回报,Set.ofLazy() 出现了,因此 List、Set 和 Map 现在都有惰性变体。

这是本发行版中唯一新增的 API 元素。

4.3. 加密对象的 PEM 编码

PEM 是一种文本格式,用标记包裹 Base64 编码的 DER 数据,我们用它来处理证书、CRL 和密钥。在此 API 之前,JDK 没有提供读取或写入这些数据的标准方式。我们要么去掉标记并手动解码 Base64,要么求助第三方库。

PEMEncoder 和 PEMDecoder 处理两个方向,而 withEncryption(char[]) 和 withDecryption(char[]) 覆盖受密码保护的私钥。

第三个预览版主要是细化:

  • DEREncodable 变为 BinaryEncodable,这个名称不再把接口与 DER 绑定。
  • X509Certificate 和 X509CRL 现在实现它。EncryptedPrivateKeyInfo 新增了直接加密和解密密钥的方法。
  • 新增的 CryptoException 报告失败,并添加了接收 Base64 编码字节数组的构造函数。

4.4. 模式、instanceof 和 switch 中的原始类型

模式匹配和 switch 长期以来一直适用于引用类型,但在原始类型上止步。JEP 532 弥补了这一差距:instanceof 和 switch 接受原始类型模式,并且只有当值能够无损地放入目标类型时,模式才匹配。

因此,以下示例是有效的:

$ java
static String describe(int value) {
    return switch (value) {
        case byte b -> "fits in a byte: " + b;
        case short s -> "fits in a short: " + s;
        default -> "needs an int: " + value;
    };
}

Java 27 原样重新提交了该特性,与 Java 26 相比没有变化。 它自 Java 23 以来一直处于预览阶段。

4.5. Vector API

Vector API 让我们能够表达由 JIT 编译为 CPU 向量指令的计算。

该 API 仍处于孵化阶段,与 Java 26 相比没有变化。 唯一的区别是捆绑的 SLEEF 库, 它在 ARM 和 RISC-V 上提供向量数学内建函数,并从 3.6.1 升级到 3.9.0。

十二轮孵化听起来像是一个停滞的特性,但其原因在于依赖关系。该 API 在等待来自 Project Valhalla 的值类型, 以便向量值可以被扁平化而不是分配。JEP 401 此后已作为预览集成到 JDK 28 中,因此下一轮可能就是让 Vector API 走出孵化阶段的那一轮。

5. 较小变更与移除

除了 JEP 之外,Java 27 还为类库和工具带来了若干较小变更:

  • Math 和 StrictMath 新增反双曲函数 acosh()、asinh() 和 atanh()
  • String.encodedLength(Charset) 返回给定字符集中的字节长度,而无需构建数组
  • BigDecimal.rootn(int, MathContext) 计算 n 次方根
  • DateTimeFormatter 接受诸如 +02 这样的短时区偏移
  • 从另一个 HashMap 复制时,HashMap.putAll() 快了 66%–86%
  • jcmd VM.security_properties 打印正在运行的 JVM 的有效安全属性

JVM 编译器接口(JVMCI) 已被移除。它是一个用于将外部构建的 JIT 编译器插入 HotSpot 的插槽,而它的主要用户 Graal 编译器已经在 Java 17 中离开了 JDK。GraalVM 发行版和 Native Image 不受影响。

6. 特性总结

Java 27 将其九个 JEP 分布在三个成熟度级别上,只有正式特性开箱即用:

特性JEP状态
默认启用紧凑对象头534正式
G1 成为所有环境下的默认垃圾回收器523正式
TLS 1.3 的后量子混合密钥交换527正式
JFR 进程内数据脱敏536正式
惰性常量531预览
加密对象的 PEM 编码538预览
模式、instanceof 和 switch 中的原始类型532预览
结构化并发533预览
Vector API537孵化

预览特性需要在编译和运行时都使用 --enable-preview,而编译器要求我们指定发行版:

$ bash
javac --enable-preview --release 27 Main.java
java --enable-preview Main

请注意,Vector API 不是预览特性,而是一个孵化模块,因此它使用不同的标志:

$ bash
javac --add-modules jdk.incubator.vector Main.java
java --add-modules jdk.incubator.vector Main

这两类 API 都可能在下一个版本中发生变化或消失,因此它们只适合用于实验,而不是生产代码。

7. 结论

Java 27 是一个我们会在运行时而不是代码中察觉到的版本:更小的对象、所有环境统一的垃圾回收器、抗量子的 TLS 握手,以及不再泄露凭据的记录。

没有任何特性从预览转为正式,因此没有新的生产 API 可供构建。但 Java 29 将是下一个 LTS,而现在针对 27 进行测试,可以让我们提前一个版本发现过时的启动标志,以及较旧网络设备会拒绝的 TLS 握手,而不是在 LTS 升级时一次性全部暴露。

文章 Java 27 中的新特性 首次出现在 Baeldung。