JEP draft: Strict Field Initialization in the JVM (Preview)

OwnerDan Smith
TypeFeature
ScopeSE
StatusDraft
Componenthotspot / runtime
Discussionvalhalla dash dev at openjdk dot java dot net
EffortM
DurationL
Created2025/02/20 21:25
Updated2025/10/04 01:23
Issue8350458

Summary

Support a JVM flag on fields expressing that the field must be properly initialized before it can be read. Enforce this requirement during verification and at run time, and provide optional run-time diagnostics for static fields that have not set the flag.

Goals

Non-Goals

Motivation

Whenever a class is loaded by the JVM, it needs to be initialized. In bytecode, each class and interface can declare a special class initialization method, named <clinit>, for this purpose. The class initialization method is free to execute arbitrary code, and what constitutes an "initialized" class is up to the discretion of the class author. Usually class initialization includes setting all of the class's static fields to an appropriate initial value.

Similarly, whenever a class instance is created with the new bytecode, that instance needs to be initialized. In bytecode, each class can declare multiple special instance initialization methods, named <init>, for this purpose. These methods are also free to execute arbitrary code, and through a chain of <init> method invocations every class in an inheritance hierarchy can define what constitutes an "initialized" class instance, at the discretion of each class author. Usually instance initialization includes setting all of the instance's non-static fields to an appropriate initial value.

In Java, class initialization methods are not written directly, but are an aggregation of each class's static field initializers and static initializer blocks. Instance initialization methods are mainly expressed with constructors, and delegation between constructors is expressed with super(...) or this(...) calls.

Sometimes, during the initialization process, the fields of the class or instance being initialized are read. These fields may not yet have been set to an initial value, and can be thought of as being in a temporary larval state. The longstanding behavior of the JVM is to give larval fields a default valuenull for references and 0 for primitives—to be returned if they are read before being properly initialized. An early read is often a bug, but execution simply proceeds with the given value.

For example, given the following classes (styled as Java code for readability), if the class App is initialized first, no exception will be thrown, but log messages will be prefixed with an unexpected identifier, App[0]: .... This is because the invocation of Log.currentPID() implicitly triggers initialization of the Log class, and the <clinit> method of Log then reads the larval appID field, embedding its value 0 into the prefix string. Eventually, the currentPID() call will proceed, and appID will be properly initialized with the current process's ID number; but that will be too late for the prefix field.

class App {

    public static final long appID;

    static void <clinit>() {
        appID = Log.currentPID();
    }
}

class Log {

    private static final String prefix;

    static void <clinit>() {
        prefix = "App[%d]: ".formatted(App.appID);
    }

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

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

Notice that the circular dependency between classes App and Log is not obvious or essential; if the utility method currentPID were declared in some other class, the dependency would be broken and everything would behave properly. In complex systems, these sorts of bugs can be very hard to recognize and diagnose.

In this example, it is especially bad that the appID field exposes its default value, because it is a final field. If other classes later refer to appID, they'll see a different identifier than the one observed by Log. An apparently-immutable variable has been observed to mutate, with unpredictable results.

In the Java language, there are limited compile-time restrictions on field initialization that prevent some obvious bugs, like a forward reference to a field of the same class, or a failure to assign a value to a final field. javac also provides optional warnings that discourage virtual method calls in constructors. The JVM has its own guardrails, such as preventing concurrent access to a class while it is being initialized.

But the Java language and JVM are not designed to prevent all bugs that could arise from an unwanted interaction with a class or instance that is not fully initialized. Classes are at risk once their <clinit> code begins running (including due to indirect events like the loading or initialization of other classes). Instances are at risk once an <init> method shares the current instance with some other code (including the <init> method of a superclass). But classes and instances cannot be considered fully initialized until these initialization methods complete their execution.

That said, if we just focus on field initialization, we can improve the situation so that the fields of the class or instance satisfy two important invariants: first, that they have been assigned a value before they are read; and second, that if they are final, they will not be subsequently mutated.

Under these rules, the above attempt to read App.appID in its larval state would not be permitted, and developers would realize that they needed to address the failure by restructuring the two classes.

Future enhancements to the Java language and VM will be able to take advantage of these field invariants to support features like value object, which depend on reliable final fields, and non-null variables, which are incompatible with a null default value.

Description

In JDK NN, this JEP introduces a new flag for fields in a class file, ACC_STRICT_INIT. This flag is a preview VM feature, disabled by default and only allowed in classes with a preview class file version number (XX.65535). To load preview class files at run time, you must enable preview features:

java --enable-preview Main

The implications of the new flag are described below; but first, the next two sections provide some background on the class and instance initialization processes in the JVM.

Class initialization

A class is initialized by a class initialization method, <clinit>. Class initialization methods typically set up the static fields of the class, and might also interact with other global state. Each class in a hierarchy may have its own <clinit> method, and every superclass must be initialized before executing the <clinit> method of a subclass.

An initialization state is used to keep track of the initialization status of each class at run time. In today's JVM (see JVMS 5.5), a class's initialization state may be any of the following:

The <clinit> method executes while the class is in a larval state. The class is not yet initialized at this point, but its fields and methods can be freely accessed by code running in the current thread. If the <clinit> method completes successfully, the class transitions to the initialized state. If an exception occurs, the class transitions to the erroneous state and can never become initialized.

The constraints on class initialization are enforced dynamically, at run time. For example, each getstatic instruction is responsible for checking the initialization state of the resolved field's class, and if the class is in the larval state in another thread, blocking until initialization completes.

Instance initialization

An object is initialized by an instance initialization method, <init>. Instance initialization methods typically set up the instance fields of the class, and might also interact with static fields and global state. Each class in a hierarchy has at least one <init> method, and that method must, at some point before it completes, delegate to another <init> method of the current class or its superclass. (This recursion bottoms out at Object.<init>.)

Like classes, objects have an initialization state, although this is expressed only indirectly in the JVM Specification. Today, an object's initialization state may be any of the following:

An <init> method begins execution in the early larval state. Most operations, including method invocations, are not allowed on an object in the early larval state, and the object may not be shared with other code. However, its fields may be assigned with putfield. At some point another <init> method is invoked and the initialization process continues recursively, eventually reaching Object.<init>. At that point, the instance transitions to the late larval state and, one by one, delegating <init> methods complete their execution and return. In the late larval state, the object is not yet fully initialized, but use of its fields and methods is unrestricted. (It may even be shared across threads.) The object is initialized once the outermost <init> method returns successfully. Alternatively, any <init> call in the stack might fail with an exception; in that case, the object transitions to the erronous state and can never become initialized.

The constraints on instance initialization are enforced statically, by the verifier. Verification does not attempt to distinguish between the late larval and initialized states—both can be considered unrestricted. Every instruction in an <init> method is associated with either the early larval state or unrestricted. (This is tracked through the flagThisUninit flag in the instruction's type state.) The verifier prevents most operations on the current object in the early larval state, and ensures the unrestricted state can only be reached via a delegating <init> call. The return instruction is only allowed in the unrestricted state.

Strictly-initialized fields

The new ACC_STRICT_INIT (0x0800) flag may be set in the access_flags item of any field declaration. This indicates that the field is strictly-initialized. (Before Java SE 17, the ACC_STRICT flag, also 0x0800, was applied to methods to indicate a requirement for "strict" floating-point semantics. That capability was removed by JEP 306. The two flags are unrelated.)

Strictly-initialized fields must be initialized—that is, they must be assigned an initial value—during the larval phase of class initialization and the early larval phase of instance initialization. These fields have no observable default value, and may not be read until their initial assignment has occurred.

These constraints are enforced by enhancing the representation of the larval and early larval initialization states to track whether each field has been set. Then for static fields, new checks are performed dynamically during class initialization; for instance fields, new checks are performed statically by the verifier.

The following rules apply during the class initialization process to ensure all strictly-initialized static fields are properly initialized:

The following rules apply during verification of an <init> method to ensure all strictly-initialized instance fields are properly initialized:

(It has never been allowed to perform getfield on the current class instance in an early larval state.)

Stable final fields

A strictly-initialized final field must not be observed to mutate—all reads must have the same value. Assignments to the field are only allowed during the larval phase of class initialization and the early larval phase of instance initialization.

In some complex cases, such as due to exception handling, a final field may be written multiple times during initialization. This is allowed, but any intermediate values of the field cannot be read.

During class initialization:

During verification of an <init> method:

Verification enhancements

Some additional changes to class file verification are necessary to account for early larval initialization states that keep track of field initialization.

One of the requirements of verification is that for every jump in a method, including every implicit jump to an exception handler, the type state of the jump target must be compatible with the incoming type state at the point of the jump. Each jump target declares its type state in the StackMapTable attribute.

Because initialization state is part of the type state, and because jumps may occur in the early larval code of <init> methods, it is necessary to enhance the StackMapTable attribute to be able to express early larval initialization states, including enumerating the fields that have been set in an early larval state.

(Alternatively, we could require all field-setting code to occur immediately before the delegating <init> method invocation, without any jumps. But this would be an inconvenient restriction.)

Entries in the StackMapTable are typically expressed as modifications of the previous frame. In an <init> method, the initialization state of the implicit first stack map frame is early larval, where all ACC_STRICT_INIT instance fields declared by the class are considered unset.

From that point:

A jump target can act as a join point for multiple execution paths, and the incoming initialization state from these two paths may differ. For example, a field may be assigned on one path, and not assigned on another.

The early larval state should be understood to track which fields are guaranteed to be set; a possibly-unset field is expressed just like a definitely-unset field. A jump can always transition from one early larval state to another as long as the transition only "unsets" some fields. Initialization state transitions to an early larval state with more fields set, or to the unrestricted state, can only be achieved via the putfield and invokespecial instructions, respectively.

It is not possible for a jump to join early larval and unrestricted code paths, or to handle the erroneous state that occurs when a delegating invokespecial throws an exception. This is a longstanding limitation (with a messy bug tail); it could be addressed in the future by supporting an explicit erroneous_frame in the StackMapTable.

Reflective initialization

Various libraries allow fields to be assigned and read without using bytecode instructions. These include java.lang.reflect.Field, java.lang.invoke.MethodHandle, and java.lang.invoke.VarHandle.

For strictly-initialized static fields, assignments and reads expressed through library code perform the same checks and have the same effects on the class initialization state as putstatic and getstatic, as described above:

For strictly-initialized instance fields, note that the verifier prevents libraries from interacting with objects in the early larval state. Until the object reaches the late larval phase, the initialization state of the object may only be manipulated with bytecode instructions in an <init> method. This means that a strictly-initialized field cannot be assigned its initial value by a library, and a strictly-initialized final field cannot be mutated by a library.

This restriction on instance fields is inconvenient for tools that perform their own object initialization for user-defined classes, but is necessary to support the invariants of strictly-initialized fields. These tools must, necessarily, cooperate with the class's <init> methods to initialize any strictly-initialized fields.

Some standard libraries require changes to ensure they cannot be used to circumvent the constraints on strictly-initialized instance fields:

Run-time optimization of strictly-initialized fields

The invariants of fields marked with ACC_STRICT_INIT provide the JVM with opportunities to optimize uses of those fields at run time.

For example, in JDK NN HotSpot's JIT compiler treats strictly-initialized final fields as trusted. A trusted final field is known to never change, so once a value has been read from it, subsequent reads can re-use that same value.

Thus, in the following loop, if this.size is strictly-initialized and final, the size value that gets read at the start of the loop can reliably be re-used in the bounds check after each iteration, without worrying that doSomething() may have had the side effect of mutating size.

for (int i = 0; i < this.size; i++) {
    doSomething();
}

The resulting JIT-compiled code has fewer interactions with memory and may execute faster.

Static field initialization diagnostics

The runtime checks performed for strictly-initialized static fields will often be useful even for fields that are not marked with ACC_STRICT_INIT. As a debugging tool, HotSpot can provide class initialization diagnostics via the command-line flag -XX....

When these diagnostics are turned on, all static fields are tracked by the class initialization state. Whenever any non-strict static field is read before it has been initialized or, in the case of a final field, mutated after it has been read, a diagnostic is generated.

The command-line flag specifies whether the diagnostic takes the form of a fatal error or an event logged to the console and JFR.

Testing

The ACC_STRICT_INIT flag is not a language feature, but it is often convenient to write HotSpot tests in Java code.

For testing, this JEP introduces an OpenJDK test library to:

Supporting changes

The Field.accessFlags and Field.getModifiers methods should reflect the presence of ACC_STRICT_INIT.

The java.lang.classfile API should support ACC_STRICT_INIT and early_larval_frame entries in StackMapTable. When a StackMapTable is automatically generated, it should properly encode the initialization state of strictly-initialized fields.

The javap tool should properly display the ACC_STRICT_INIT modifier and early_larval_frames; it could also do a better job of presenting the implicit initialization states in a StackMapTable.

The asmtools tools should similarly be updated to support ACC_STRICT_INIT and early_larval_frame.

Internal JVM optimizations may use the ACC_STRICT_INIT flag to reason about the timing of potential changes to a final field's value. Other tools and APIs may also depend on the flag for their own analyses.

Alternatives

Dependencies

Value Classes and Objects builds on this JEP, marking all the fields of value classes ACC_STRICT_INIT, and encouraging programming patterns in Java that initialize fields in the early larval phase.