返回

文章详情

JEP 539: JVM中的严格字段初始化移至预览

Hacker News2026年7月2日 18:56

摘要 引入在Java虚拟机中严格初始化的字段。这些字段必须在读取之前被初始化,因此诸如0或null的默认值永远不会被观察到。对于被定义为final的严格初始化字段,总是观察到相同的值。这是一个预览VM功能,可供发出类文件的编译器使用。目标 为基于JVM的编程语言的设计者提供一个字段初始化模型,其完整性保证比现有模型更强。为这些设计者提供灵活性,以便对于类中的每个静态和实例字段选择是否选择新的模型或继续使用现有模型。非目标 不是引入新的Java语言特性,例如字段的严格初始化修饰符。也不是为了改变javac编译策略以强制对现有Java源代码实施严格字段初始化。动机 Java平台规定,每个变量在使用之前必须初始化,确保程序永远无法从未初始化的内存中读取。如果类中的字段(无论是静态字段还是实例字段)未显式初始化,则在使用之前会隐式初始化,设置为默认值。该值始终是某种形式的零:数字0、布尔值false或null引用。默认值是一个混合的祝福。它们提供了一个简单的安全网,确保程序从未观察到未初始化的内存,但它们经常被误解为合法数据,而不是信号表明尚未写入任何内容。例如,方法可能会从字段中读取一个null值,然后将其传递给其他方法和构造函数,结果在读取字段的地方很远的地方触发NullPointerException。JDK 14改进了这种异常中的消息,使其更容易确定特定代码行中错误的来源,但这些消息无法将您引导回最初提供null的初始化错误。此外,Java平台还规定,声明为final的变量不能被修改,确保对final变量的任何两个读取产生相同的值。然而,对于final字段,在类或实例被初始化时,该规则不适用。因此,程序可能会在字段设置为其预期值时在不同时间读取不同的值。字段初始化错误的实际情况 下面的示例说明了意外默认值和不一致final字段的问题。在这些类中,final字段App.appID可能在Log类中的代码读取之前尚未赋予其正确的值。当发生这种情况时,不同的程序组件最终会使用冲突的字段值。 class App { public static final long appID = Log.currentPID(); // [1], [4], [6] public static void main() { IO.println("App[" + appID + "] has started"); // ... Log.log("Completed 'main'"); } } class Log { // [2] private static final String prefix = "App[" + App.appID + "]: "; // [3] public static void log(String msg) { IO.println(prefix + msg); } public static long currentPID() { return ProcessHandle.current().pid(); // [5] } } 当从命令行运行类App时,输出如下: App[96052] has started App[0]: Completed 'main' ID号之间的差异产生是因为在App类中调用Log.currentPID() [1] 触发了Log类的初始化 [2],在该类的初始化期间,读取了appID字段的默认0值 [3]并嵌入到前缀字符串中。在Log类初始化后,从App类调用其currentPID方法 [4] 继续执行,生成当前进程的ID号 [5],最终被分配给App.appID [6]。然而,该赋值对前缀字段来说为时已晚。在复杂的系统中,这类错误难以被识别和诊断。一个微妙之处是初始化的顺序很重要:如果首先初始化Log类,则不会观察到差异。另一个微妙之处是,App类和Log类之间的循环依赖很容易出错并且在后期容易忽视;如果当前PID方法在某个其他类中声明,则不存在循环性,一切都将如预期那样行为。大多数类型的Java变量都没有遭受这些问题。局部变量必须在读取之前显式赋值,而final局部变量只能赋值一次。字段在依赖默认值方面是独特的。对字段初始化的严格方法 我们提出了一种替代方案来初始化字段,包括非final和final字段。我们不再使每个字段在创建时初始化为默认值,而是修改JVM以确保一些被指定为严格初始化的字段在允许读取之前在字节码中显式初始化。像javac这样的编译器负责

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡