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

OwnerDan Smith
TypeFeature
ScopeSE
StatusDraft
Componenthotspot / runtime
Discussionvalhalla dash dev at openjdk dot org
EffortM
DurationL
Created2025/02/20 21:25
Updated2026/01/15 01:27
Issue8350458

Summary

Support a new kind of field in the JVM that must be explicitly initialized before it can be read. Enforce this requirement during verification and at run time, and provide optional run-time diagnostics for the initialization of traditional static fields.

Goals

Non-Goals

Motivation

The Java Platform specifies that all variables are initialized before use, and a program can never read from uninitialized memory. For the fields of a class—both instance fields and static fields—this is handled by implicitly setting the field to a default value before any code in the class is run. This value is always some form of "zero": the number 0, the boolean false, or a null reference.

Default values are a mixed blessing: they provide a straightforward safety net ensuring the program never observes uninitialized memory, but they can often be misinterpreted as legitimate data, not just a "nothing written yet" signal.

A null value, for example, may be read from a field and then passed on to other methods and constructors, only to trigger a NullPointerException somewhere far from where the field was read. Since Java 14, Helpful NullPointerExceptions have made it easier to pinpoint the source of the error within a line of code, but these error messages can't direct the developer back to the initialization bug that supplied the null in the first place.

The Java Platform also specifies that, usually, variables declared final cannot be mutated, and any two reads from a final field will produce the same value. But it is possible to violate this rule using deep reflection; and even more fundamentally, a program can read from a final field while the class or instance is still being initialized, producing a default value on one read and the actual assigned value on the next.

To illustrate both of these problems: in the following classes, the final field appID may be read by Log before it has been assigned its proper value. If this happens, different program components may end up working with different field values.

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

    public static void main() {
        IO.println("App[" + appID + "] has started");
        ...
        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();
    }
}

When the class App is run from the command line, the output looks like:

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

The discrepancy between ID numbers arises because the invocation of Log.currentPID() by class App implicitly triggers initialization of the Log class, and during that class's initialization, the default value of the appID field is read. That 0 value is then embedded into the prefix string. Eventually, the currentPID() call will proceed, and appID will be properly set to the current process's ID number; but that will be too late for the prefix field.

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. Also notice that the order of initialization matters: if the Log class gets initialized first, the bug does not occur. In complex systems, these sorts of bugs can be very hard to recognize and diagnose.

Most kinds of Java variables do not suffer from these problems: a local variable must be explicitly assigned before it is read, and a final local variable may only be assigned once. Fields are unique in their reliance on default values.

This JEP proposes an alternative approach to field initialization. This approach does not rely on default values, and supports two fundamental invariants:

In order to unlock these benefits, however, programs must adopt new initialization rules and behaviors. Existing Java programs cannot simply be re-interpreted with new semantics. Instead, this JEP focuses on establishing a foundation in the JVM that new language features can build on. Language compilers will be responsible for generating fields that opt in to the alternative initialization discipline, and when they do, developers and the JVM itself can rely on the new invariants.

For example:

By building a better foundation for field initialization in the JVM, we can support features like these and also lay the groundwork for a future in which unexpected default values and unstable final fields are a thing of the past.

Description

In JDK NN, this JEP introduces a new flag for fields in a class file, ACC_STRICT_INIT (0x0800), which 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, beyond their similar names.)

This flag is a preview VM feature, disabled by default and only recognized 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

Strictly-initialized fields do not have default values. They may never be read before they have been set, and if they are final, all subsequent reads produce the same value.

Below, we'll review the class initialization process in the JVM and discuss new rules for strictly-initialized static fields; then we'll review the instance initialization process and discuss new rules for strictly-initialized instance fields.

Class initialization

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; it may also involve interactions with global state.

In Java code, class initialization methods are not written directly, but are an aggregation of each class's static field initializers and static initializer blocks.

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.

Classes that have started but not finished their initialization process can be considered larval: developing, but not yet fully-formed.

An initialization state is used to track the 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. If the class is not initialized, but is in a larval state in another thread, getstatic blocks until initialization completes.

Tracking static field state

With this JEP, the larval class initialization state is enhanced to keep track of whether each static field of the class has been set, and whether each static field of the class has been read.

When executing a putstatic or getstatic instruction, if the resolved field is declared by a class in a larval state in the current thread, the state is updated to record that the field has been set (by putstatic) or read (by getstatic). This occurs even if the field is accessed from another method or class, and even if the field is referenced as a member of a subclass.

Some fields are declared with a ConstantValue attribute, and these fields are always considered set.

With this information, the JVM can enforce the invariants of strictly-initialized static fields as follows:

(In some complex cases, such as due to exception handling, a static final field may be written multiple times during initialization. This is allowed, but only the ultimate value of the field will be readable.)

Once a class has transitioned to the initialized state, all its strictly-initialized fields have been set, and the initialization state no longer needs to keep track of static field state.

Static field initialization diagnostics

Static fields that have not been designated strictly-initialized can also benefit from tracking their state during class initialization. As a debugging tool, HotSpot provides class initialization diagnostics via the command-line flag -XX:CheckAllStaticsStrictly=[warn|error|jfr] or -Xlog:strict+static=warning.

With these diagnostics turned on, whenever any non-strict static field is read during class initialization before it has been set 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.

Instance initialization

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 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 object's instance fields to an appropriate initial value; in may also involve interactions with static fields or global state.

In Java code, instance initialization methods are mainly expressed with constructors, and delegation between constructors is expressed with super(...) or this(...) calls. Instance initialization methods may also aggregate a class's instance field initializers and instance initializer blocks.

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>.

Instances that have started but not finished their initialization process can, like classes, be considered larval: developing, but not yet fully-formed.

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 restricted larval state. Most operations, including method invocations, are not allowed on an object in the restricted 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 unrestricted larval state and, one by one, the recursively invoked <init> methods complete their execution and return. In the unrestricted larval state, use of the object, including its fields and methods, is unrestricted. (The object 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 determines a type state for each instruction, and that type state is either restricted (for objects in the restricted larval state) or unrestricted (for objects in the unrestricted larval and initialized states, and for static method bodies). A restricted type state is indicated with flagThisUninit.

For instructions operating on restricted type states, the verifier prevents most operations on the current object, and ensures that an unrestricted type state can only be reached via a chain of recursively delegating <init> calls that eventually reaches Object.<init>. The return instruction, which makes a newly constructed object available to the caller of <init>, is only allowed in an unrestricted type state.

Tracking instance field state

With this JEP, the restricted larval instance initialization state is enhanced to keep track of whether each instance field of the class has been set.

In the verifier, this is expressed with a restricted type state that carries a list of all the current class's strictly-initialized instance fields that have not yet been set. A putfield on the current class instance in a restricted type state removes the named field from the list.

The enhanced type state supports the following rules to enforce the invariants of strictly-initialized instance fields:

It has never been permitted to use getfield on an instance in a restricted type state. Thus, there is no rule for getfield analogous to the getstatic rule for static fields, and no need to track whether final fields have been read.

These verification rules ensure that all strictly-initialized fields of an object will be set while it is in a restricted larval state, before any reads can occur, and that no strictly-initialized final fields will be mutated once the object enters the unrestricted larval state. At run time, there is no need for additional checks to enforce the initialization invariants.

StackMapTable enhancements

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.

In the past, the StackMapTable was not concerned with restricted type states—whether a type state was restricted or not could be inferred from the types it contained. But now, in some cases it will be necessary to express the list of unset fields associated with a restricted type state.

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 restricted, where all strictly-initialized 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 type states from these two paths may differ. For example, a field may be assigned on one path, and not assigned on another.

Restricted type states 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 restricted type state to another as long as the transition only "unsets" some fields. Transitions to a restricted type state with more fields set, or to an unrestricted type state, can only be achieved via the putfield and invokespecial instructions, respectively.

It is not possible for a jump to join restricted 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 static fields, assignments and reads expressed through library code perform the same checks and have the same effects in a larval class initialization state as putstatic and getstatic, as described above:

For instance fields, note that the verifier prevents libraries from interacting with objects in a restricted larval state. Until the object reaches the unrestricted larval state, 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 assigned at all 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.

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 restricted_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 restricted_frames; it should 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 restricted_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 restricted larval state.