JEP draft: Strict Field Initialization in the JVM (Preview)
| Owner | Dan Smith |
| Type | Feature |
| Scope | SE |
| Status | Draft |
| Component | hotspot / runtime |
| Discussion | valhalla dash dev at openjdk dot org |
| Effort | M |
| Duration | L |
| Created | 2025/02/20 21:25 |
| Updated | 2026/01/15 01:27 |
| Issue | 8350458 |
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
-
Allow class files to opt in to a more structured initialization discipline for fields, ensuring that the fields are always set before they are read and, if
final, never modified after they are read. -
Enable run-time optimizations for these fields by enforcing the initialization discipline via verification-time and run-time checks. Enhance the
StackMapTableattribute as necessary to express field initialization status during construction. -
Provide tools to diagnose initialization bugs releated to static fields, even when those fields have not opted in to the new discipline.
Non-Goals
-
It is not a goal to introduce any new Java language features, such as a new modifier for fields.
-
It is not a goal to change
javaccompilation strategies in order to adopt the feature in the bytecode of existing Java programs.
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:
- A field read always observes a previously-written value
- Two reads of a final field always observe the same value
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:
-
The Java language intends to support value classes, new kinds of classes whose instances lack identity and can never be mutated. It is essential that the final instance fields of a value class instance always be observed with the same value. Because value classes are a new feature, they can impose different initialization rules on their fields; the compiler can then indicate the special treatment in the class file, and the JVM can enforce the expected invariants.
-
The Java language also intends to support null-restricted fields. It is essential that these fields, both static and instance, never be observed with the value
null. That rules outnullas a default value, and in many cases there is no suitable alternative. Instead, these fields need to have a valid, non-nullvalue explicitly assigned to them before they can be read. Fortunately, as a new feature, null-restricted fields can be required to conform to new initialization rules; and when they do, the compiler can indicate their special treatment in the class file, and the JVM can enforce the expected invariants.
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:
-
Uninitialized: The class is loaded but has not yet attempted initialization.
-
Larval (within a particular thread): The class is currently being initialized.
-
Initialized: The class has successfully completed initialization, and can be used without restriction.
-
Erroneous: The class failed initialization and may not be used.
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:
-
If a
getstaticinstruction attempts to read from a strictly-initialized field declared by a class in a larval state, and that field is not yet set, an exception is thrown, indicating that the field cannot yet be read. -
If a
putstaticinstruction attempts to write to a strictly-initialized final field declared by a class in a larval state, and that field has already been read, an exception is thrown, indicating that the field can no longer be set. -
Just before a class transitions to the initialized state, its larval state is checked to ensure that every strictly-initialized static field has been set; if not, an exception is thrown, indicating that the field must be explicitly set during class initialization.
(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:
-
Uninitialized: The object has been created by
new, but has not yet attempted initialization. -
Restricted larval: The object is currently being initialized, and limited operations are available.
-
Unrestricted larval: The object is currently being initialized, but can be used without restriction.
-
Initialized: The object has successfully completed initialization.
-
Erroneous: The object failed initialization and may not be used.
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:
-
An
invokespecialof an<init>method, applied to the current class instance in a restricted type state, requires that if the invocation is of a superclass method, the list of unset fields must be empty. (If the invocation is of another<init>method of the same class, there is no such requirement—the invoked method is responsible for setting the fields.) -
A
putfieldinstruction writing to a strictly-initialized final field of the current class is only allowed in a restricted type state. (In contrast,putfieldis allowed throughout the body of an<init>method for final fields that are not strictly-initialized.)
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 frame that has
uninitializedThisas the type of one of its local variables represents a restricted type state, with the same set of unset fields as the previous frame. -
A frame that does not have
uninitializedThisas the type of one of its local variables represents an unresticted type state (and all fields are considered set). -
A frame may explicitly express a restricted state with a new kind of frame,
restricted_frame:/* NOTE: tentatively considering the renaming early_larval_frame --> restricted_frame */ restricted_frame { u1 frame_type = RESTRICTED; /* 246 */ u2 number_of_unset_fields; u2 unset_fields[number_of_unset_fields]; base_stack_map_frame base_frame; }This frame wraps a
base_framewith an explicit assertion that it is restricted, and with a list of unset fields (given byNameAndTypeconstants).
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:
-
Field assignments update the class initialization state to indicate that a field has been set. If the field is strictly-initialized and final, this operation throws an exception when, per the initialization state, the field no longer allows assignments.
-
Field reads update the class initialization state to indicate that a field has been read. If the field is strictly-initialized, this operation throws an exception when, per the initialization state, the field has not been set.
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:
-
Standard object deserialization is implemented with special permission to skip the usual execution of an
<init>method in the class being instantiated. This capability bypasses the verification-based enforcement of constraints on strictly-initialized instance fields, and must not be used for classes that declare these fields.Instead,
ObjectOutputStream.writeObjectandObjectInputStream.readObjectthrow anInvalidClassExceptionif a class being serialized or deserialized declares a strictly-initialized instance field (and the class is not a record class).Users of serialization can implement the
writeReplaceandreadResolvemethods to avoid this exception. Doing so causes a replacement object to be serialized and deserialized instead of the object that declares strictly-initialized fields.(A future enhancement to serialization is anticipated, allowing class authors to declare special constructors that
ObjectInputStream.readObjectcan use to create new instances from the data in serialization streams.) -
The
Field.setAccessiblemethod allows clients to bypass thefinalrestriction on most instance fields, enabling mutation in the late larval and initialized states. A strictly-initialized final field cannot support this behavior, and should always be treated as non-modifiable by thesetAccessiblemethod.(Relatedly, Prepare to Make Final Mean Final provides for warnings when reflection is used to mutate final fields that are not marked strictly-initialized.)
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:
-
Define a
@StrictInitannotation that can be placed on fields that should be treated as strictly-initialized; and -
Generate class files from Java sources in a two-step process, first compiling with
javac, and then rewriting the bytecode to apply theACC_STRICT_INITflag and adjust the initialization timing of<init>methods.
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
-
In JDK 21, the
javaccompiler added warnings to discourage invocations of instance methods from superclass constructors (see JDK-8299995). Such warnings are helpful, but of course are no substitute for invariants enforced by the JVM. -
We've considered approaches that enforce instance field invariants with dynamic checks. These would allow more flexibility in the timing of instance field initialization. Unfortunately, they require a run-time overhead that is not easily optimized away once the object has been fully initialized.
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.