Java 27 新特性解读:九项 JEP 落地,四项正式转正
[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 握手日志:
在记录的日志消息中,命名组就是双方商定的密钥交换。Java 27 支持三种混合组:
- X25519MLKEM768
- SecP256r1MLKEM768
- SecP384r1MLKEM1024
像 x25519 这样的普通值表示对端不支持其中任何一种。
3.2. JFR 进程内数据脱敏
Java 27 会在 JVM 内部对 JFR 记录中的敏感值进行遮蔽,在它们到达记录文件之前就完成处理。以前会出现在记录中的值,现在会显示为 [REDACTED]。
这一点很重要,因为记录会捕获命令行参数、环境变量和系统属性,而密码和令牌通常就存放在这些地方。 过去把一份记录附到支持工单上,就意味着把这些秘密也一并发送出去。
我们应当注意,脱敏是基于名称的。JVM 会将属性、变量和参数名称与一组 glob 模式进行匹配。默认设置覆盖了诸如 *password* 或 *token* 这样的名称,但我们也可以通过 JVM 标志添加自己的模式:
在这里,redact-key 适用于系统属性和环境变量的名称。 使用上面的模式,db.credential 和 client.secret 的值也会在记录中被替换,而像 auth.key 这样的名称仍会原样通过。命令行参数由单独的 redact-argument 选项处理。
4. 仍处于预览阶段
有五个特性仍处于预览或孵化阶段。其中大多数都只是带着细微改进重新提交,而有一个特性包含会破坏现有代码的变更。
4.1. 结构化并发
与第六个预览版相比,这个预览版带来了破坏性代码变更:
- joiner 现在抛出 ExecutionException,而不是 FailedException
- Joiner 新增了第三个类型参数,用于异常类型
- awaitAll() 被移除
- onTimeout() 变为 timeout()
因异常类型导致的破坏在实践中可能最常见,例如:
针对第六个预览版编写的每个 catch 子句都必须更新,而 ExecutionException 是受检异常,因此编译器会把这些地方指出来。
4.2. 惰性常量
惰性常量保存一个在首次访问时计算、之后行为如同常量的值。 JVM 可以像优化 final 字段那样优化它们,而 synchronized 块背后的普通惰性初始化无法做到这一点。典型用法是我们不愿在启动期间构建的昂贵对象。例如:
第三个预览版移除了 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 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 分布在三个成熟度级别上,只有正式特性开箱即用:
预览特性需要在编译和运行时都使用 --enable-preview,而编译器要求我们指定发行版:
请注意,Vector API 不是预览特性,而是一个孵化模块,因此它使用不同的标志:
这两类 API 都可能在下一个版本中发生变化或消失,因此它们只适合用于实验,而不是生产代码。
7. 结论
Java 27 是一个我们会在运行时而不是代码中察觉到的版本:更小的对象、所有环境统一的垃圾回收器、抗量子的 TLS 握手,以及不再泄露凭据的记录。
没有任何特性从预览转为正式,因此没有新的生产 API 可供构建。但 Java 29 将是下一个 LTS,而现在针对 27 进行测试,可以让我们提前一个版本发现过时的启动标志,以及较旧网络设备会拒绝的 TLS 握手,而不是在 LTS 升级时一次性全部暴露。
文章 Java 27 中的新特性 首次出现在 Baeldung。