本博客日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 字段初始化中最隐蔽的陷阱之一,默认值被“合法化”了。0、null、false 这些默认值虽然保证了内存安全,却常常伪装成有效数据,让 Bug 潜伏得更深、更难追踪。
而 JEP 539 要解决的,就是这个问题。
JEP 539 是什么?
官网列的全称是,JEP 539: Strict Field Initialization in the JVM (Preview)。
简单来说,JEP 539 在 JVM 层面引入了一种“严格初始化”的字段模型。
被标记为严格初始化的字段,
必须在被读取之前完成显式初始化。0、null、false这类默认值永远不会被观察到。
对于 final 字段,还有一个更强的保证,所有读取操作都观察到同一个值,彻底杜绝初始化期间的“值漂移”。
需要说明的是,它是一个预览版的 JVM 特性,通过 class 文件中的新标志 ACC_STRICT_INIT(0x0800)来标识。它不是一个 Java 语言层面的新关键字,而是供所有生成 JVM 字节码的编译器使用的底层机制。
文章配图参见我的公众号:https://mp.weixin.qq.com/s/PwIzls_qpX6fEy8Htmtulg。
默认值的伪装
在现有 Java 模型中,未显式初始化的字段会被自动赋予默认值。
不同类型对应的默认值不同,具体如下所示。
- 数值类型 →
0 - 布尔类型 →
false - 引用类型 →
null
和数据库的默认值一样,确实防止了“读取未初始化内存”的灾难,但也带来了一个副作用,程序可能把默认值当成合法数据来使用。
比如读到 null 后一路传递,最终在某个遥远的方法里触发 NullPointerException。JDK 14 改进了 NPE 的提示信息,帮助我们定位到出错的代码行,但它无法带你回到那个“本该初始化却忘了初始化”的源头。
Final 字段的临时变脸
我们都知道 final 字段是不可变的。但鲜为人知的是:在类或对象初始化期间,final 字段是可以被多次写入的。
这意味着,如果存在循环依赖(如上面的 App 和 Log),代码可能在 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。
官方给出的解决方案是实现 writeReplace 和 readResolve 方法,用替代对象进行序列化。
而对于 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 重构字段初始化