Java基础、中级、高级、架构面试资料

JDK 28 修复对象初始化重大 bug,JEP 539 重构字段初始化

JAVA herman 7浏览
公告:“业余草”微信公众号 AI 中转站提供免费体验,点击链接 https://unity2.ai/register?ref=3XTnndN2 进行访问,支持 Claude、ChatGPT、Gemini 等最新模型!关注业余草微信公众号,添加作者微信:xttblog2!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

前段时间我写文章说 Java 的值对象终于要落地了,JEP 401 现在已经加入 JDK 28 了。有了 JEP 401,Java 后续还有很多 JEP 等待着落地。

今天,我又看到 JEP 539: Strict Field Initialization in the JVM (Preview) 也加入到了 JDK 28 中。该特性是一个依赖于 JEP 401 的改动,从 JVM 底层给字段上了“防盗锁”。

所以,接下来我们就针对 JEP 539 来一次深度解读。了解并学习 JVM 严格字段初始化,以及对 Java 对象模型“安全锁”的升级。

一个让人头疼的 Bug

我们先看一段代码,一段曾让不少老外踩坑的 bug。

class App {
    public static final long appID = Log.currentPID();

    public static void main() {
        IO.println("App[" + appID + "] has started");
        Log.log("Completed 'main'");
    }
}

class Log {
    private static final String prefix = "App[" + App.appID + "]: ";

    public static void log(String msg) {
        IO.println(prefix + msg);
    }

    public static long currentPID() {
        return ProcessHandle.current().pid();
    }
}

上面这段代码,大家觉得输出会是什么?比如类似下面这样的输出。

App[96052] has started
App[96052]: Completed 'main'

上面这个输出是错的。实际输出很可能是下面这样。

App[96052] has started
App[0]: Completed 'main'

App[0],很意外吧。

因为 Log 类在初始化时读取了 App.appID,而此时 appID 还没被赋值为真正的 PID,JVM 给了它默认值 0。这个 0 被永久嵌入了 prefix 字符串。

这就是 Java 字段初始化中最隐蔽的陷阱之一,默认值被“合法化”了0nullfalse 这些默认值虽然保证了内存安全,却常常伪装成有效数据,让 Bug 潜伏得更深、更难追踪。

而 JEP 539 要解决的,就是这个问题。

JEP 539 是什么?

官网列的全称是,JEP 539: Strict Field Initialization in the JVM (Preview)

简单来说,JEP 539 在 JVM 层面引入了一种“严格初始化”的字段模型。

被标记为严格初始化的字段,必须在被读取之前完成显式初始化0nullfalse 这类默认值永远不会被观察到。

对于 final 字段,还有一个更强的保证,所有读取操作都观察到同一个值,彻底杜绝初始化期间的“值漂移”。

需要说明的是,它是一个预览版的 JVM 特性,通过 class 文件中的新标志 ACC_STRICT_INIT0x0800)来标识。它不是一个 Java 语言层面的新关键字,而是供所有生成 JVM 字节码的编译器使用的底层机制。

文章配图参见我的公众号:https://mp.weixin.qq.com/s/PwIzls_qpX6fEy8Htmtulg

默认值的伪装

在现有 Java 模型中,未显式初始化的字段会被自动赋予默认值。

不同类型对应的默认值不同,具体如下所示。

  • 数值类型 → 0
  • 布尔类型 → false
  • 引用类型 → null

和数据库的默认值一样,确实防止了“读取未初始化内存”的灾难,但也带来了一个副作用,程序可能把默认值当成合法数据来使用

比如读到 null 后一路传递,最终在某个遥远的方法里触发 NullPointerException。JDK 14 改进了 NPE 的提示信息,帮助我们定位到出错的代码行,但它无法带你回到那个“本该初始化却忘了初始化”的源头

Final 字段的临时变脸

我们都知道 final 字段是不可变的。但鲜为人知的是:在类或对象初始化期间,final 字段是可以被多次写入的。

这意味着,如果存在循环依赖(如上面的 AppLog),代码可能在 final 字段还没定稿时就读取了它,得到一个“中间值”甚至默认值。初始化完成后,这个字段确实不再变化,但伤害已经造成

为未来的语言特性铺路

JEP 539 明确提到,严格字段初始化是以下特性的基础。

  • 值类(Value Classes):JEP 401 中定义的新型类,实例没有对象身份(identity),且不可变。值类的 final 实例字段必须始终观察到相同的值,否则“值语义”就崩塌了。
  • 非空字段:未来某些字段可能不允许存储 null,这类字段自然不能依赖 null 默认值,必须在读取前显式赋值为非空值。

JVM 是如何实现的?

JEP 539 对静态字段和实例字段采用了不同的 enforcement 策略。

静态字段

JVM 在类的“幼虫态”(larval state,即类正在初始化但尚未完成的状态)中,追踪每个严格初始化静态字段的“已设置”和“已读取”状态

  • 如果 getstatic 试图读取一个尚未设置的严格初始化字段 → 抛异常
  • 如果 putstatic 试图在 final 字段已被读取后再次写入 → 抛异常
  • 类初始化完成前,JVM 会检查所有严格初始化静态字段是否都已设置,否则 → 抛异常

在运行时动态检查这些规则甚至适用于反射(Field.set/Field.get)和 VarHandle

实例字段

实例字段的约束通过增强字节码验证器(字节码验证器静态检查)来实现。

  • 在“早期幼虫态”(early-larval,即 super() 调用前),验证器维护一个“未设置字段列表”。
  • 每次 putfield 写入一个严格初始化字段,就从列表中移除该字段。
  • 在调用父类构造器 super() 前,列表必须为空。即当前类的所有严格初始化实例字段都必须已完成赋值
  • 严格初始化的 final 实例字段只能在早期幼虫态写入,进入晚期幼虫态后禁止修改。

为了支持这种新的类型状态,class 文件的 StackMapTable 属性还引入了一种新的帧类型:early_larval_frame

JIT 优化的红利

严格初始化的 final 字段会被 HotSpot JIT 编译器视为可信常量(trusted)。因为 JVM 保证这个字段的值永远不会改变,JIT 可以在首次读取后复用该值,减少对内存的重复访问,从而提升运行性能。

这些改动,对我们普通 Java 开发者来说,暂时“无感”,但长期肯定是受益的。

JEP 539 是一个JVM 特性,不是语言特性。你写 Java 代码时不会看到新的 strict 关键字。javac 也不会对现有代码启用严格初始化。

但当我们开始使用值类(Value Classes)时,编译器会自动将值类的所有字段标记为 ACC_STRICT_INIT。届时,值类的不可变性和一致性将由 JVM 硬核保证,而不是靠约定或文档。

但是对于框架和库作者,就需要时刻关注反射和序列化。JEP 539 对反射和序列化有两项重要限制:

  • 深度反射受限:严格初始化的 final 实例字段被归类为“不可修改”,与 record 类的 final 字段和静态 final 字段同级。即使使用 --enable-final-field-mutation 也无法绕过。试图通过反射 set 这样的字段会抛出 IllegalAccessException
  • 默认序列化受限:ObjectInputStream 的默认反序列化会跳过构造器,这绕过了严格初始化的验证。因此,包含严格初始化实例字段的类(非 record)如果直接使用默认序列化,会抛出 InvalidClassException

官方给出的解决方案是实现 writeReplacereadResolve 方法,用替代对象进行序列化。

而对于 Kotlin、Scala、Groovy 等 JVM 语言的编译器作者,现在有了一个更强壮的字段初始化模型可供选择。如果某种语言特性需要更强的初始化保证,可以直接利用 ACC_STRICT_INIT

与 JEP 401 的共生关系

JEP 539 和 JEP 401(Value Objects,值对象)是紧密耦合的。

值对象的核心特征:

  • 仅包含 final 字段
  • 没有对象身份(identity)
  • 完全通过字段值来区分相等性

如果值对象的字段在初始化期间能观察到不同的值,那“值语义”就是空谈。JEP 539 提供的严格初始化保证,正是 JEP 401 的底层基石

2026 年 7 月底,JEP 539 和 JEP 401 双双从“候选”升级为“已定向”,目标版本均为 JDK 28。这意味着,在 JDK 28 中,我们将首次预览体验到值对象及其背后的严格初始化机制。

总结

JEP 539 不像 Pattern Matching、Virtual Threads 那样有 flashy 的语法糖,但它是 JVM 对象模型的一次深层加固

维度现有模型JEP 539 严格初始化
默认值0/null/false 可被观察到默认值不可见,必须显式初始化
final 一致性初始化期间可能观察到不同值所有读取保证相同值
错误发现运行时 NPE,定位困难初始化阶段即抛异常或验证失败
JIT 优化final 字段需保守处理可标记为 trusted,减少内存访问

它不会立刻改变我们写 Java 代码的方式,但它正在为值类、非空类型等下一代 Java 特性铺设轨道。当这些特性到来时,我们或许会发现 Java 的对象模型变得更加严谨、安全和高效。

期待 Java 再次伟大!期待更多新特性尽快到来!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » JDK 28 修复对象初始化重大 bug,JEP 539 重构字段初始化