Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,827 words · 5 segments analyzed
Summary Introduce strictly-initialized fields in the Java Virtual Machine. Such fields must be initialized before they are read, thus default values such as 0 or null are never observed. For strictly-initialized fields that are final, the same value is always observed. This is a preview VM feature, available for use by compilers that emit class files. Goals
Offer designers of JVM-based programming languages a model for field initialization which has stronger integrity guarantees than the present model.
Give these designers the flexibility to choose, for each static and instance field in a class, whether to opt in to the new model or continue with the present model.
Non-Goals
It is not a goal to introduce new Java language features, such as a strictly-initialized modifier for fields.
It is not a goal to change javac compilation strategies in order to impose strict field initialization on existing Java source code.
Motivation The Java Platform specifies that every variable is initialized before use, ensuring that a program can never read from uninitialized memory. If a field in a class — whether a static field or an instance field — is not initialized explicitly then it is initialized implicitly before it is used, by being set to a default value. 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 that a program never observes uninitialized memory, but they can often be misinterpreted as legitimate data rather than as a signal that nothing has yet been written. For example, a method may read a null value from a field and then pass that on to other methods and constructors, only to trigger a NullPointerException somewhere far from where the field was read. JDK 14 improved the messages in such exceptions to make it easier to pinpoint the source of the error in a specific line of code, but these messages cannot direct you back to the initialization bug that supplied the null in the first place. The Java Platform also specifies that variables declared final cannot be mutated, ensuring that any two reads of a final variable produce the same value. For final fields, however, this rule does not apply while the class or instance is being initialized.
A program may thus read different values at different times as the fields are set to their intended values. Field initialization bugs in practice The following example illustrates the problems of unexpected default values and inconsistent final fields. In these classes, the final field App.appID may be read by code in the Log class before it is assigned its proper value. When that happens, different program components end up working with conflicting field values. 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] }
} When the class App is run from the command line, the output is something like: App[96052] has started App[0]: Completed 'main' The discrepancy between ID numbers arises because the invocation of Log.currentPID() in the App class [1] triggers initialization of the Log class [2], and during that class's initialization, the default 0 value of the appID field is read [3] and embedded into the prefix string. After the Log class is initialized, the call to its currentPID method from the App class [4] proceeds, producing the current process's ID number [5], which is finally assigned to App.appID [6]. That assignment is, however, too late for the prefix field. In complex systems, these sorts of bugs are difficult to recognize and diagnose. One subtlety is that the order of initialization matters: If the Log class is initialized first, the discrepancy is not observed. Another subtlety is that the circular dependency between the classes App and Log is easy to create by mistake and easy to overlook later; if the utility method currentPID were declared in some other class, the circularity would not exist and everything would behave as expected. 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 upon default values. A strict approach to field initialization We propose an alternative approach to initializing fields, both non-final and final. Instead of every field being initialized to a default value when it is created, we alter the JVM to ensure that some fields, designated strictly-initialized, are explicitly initialized in bytecode before they are allowed to be read. Compilers such as javac are responsible for choosing which fields are designated strictly-initialized based on the language features used in source code. We call this strict field initialization because it imposes additional restrictions on the code that initializes fields. Strict field initialization makes it impossible to have unexpected default values and inconsistent final fields. Every read from a strictly-initialized field observes a previously-written value and, if the field is final, every read observes the same value. These properties are what we already intuitively expect from fields; strict field initialization promotes these properties from mere intuitions to actual integrity guarantees, enforced by the JVM. Strict field initialization improves integrity Strict field initialization lays the foundation for two new Java language features:
Value classes are 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 to have the same value.
Null-restricted fields are fields that can never store null. It is essential that these fields, both static and instance, not use null as a default value. They must be explicitly initialized with a non-null value before they can be read.
As shown above, the process of field initialization can be delicate. The JVM must not impose new initialization behavior upon existing programs since they could depend upon the existing behavior. New language features, by contrast, can define new rules and behaviors for field initialization and then adopt strict field initialization. As the language evolves and new features are adopted, program components will gradually be hardened against field initialization bugs. Description A strictly-initialized field does not have a default value. It cannot be read before it has been explicitly initialized and, if it is final, all reads produce the same value.
Compilers mark fields that are subject to strict initialization with a new flag in the class file, ACC_STRICT_INIT (0x0800). For strictly-initialized fields, the JVM enforces these invariants:
For a static field, a read cannot access the field before it is initialized, and the field must be initialized before class initialization completes. If the field is final, a write cannot mutate the field after it has been read. Violating any of these constraints causes an exception to be thrown.
For an instance field, a read cannot access the field before the super() constructor is invoked, and the field must be initialized before the super() constructor is invoked. If the field is final, a write cannot mutate the field after the super() constructor is invoked. Violating any of these constraints causes bytecode verification to fail.
The invariants of strictly-initialized fields give the JVM new opportunities to optimize uses of those fields. For example, the HotSpot JVM's JIT compiler will treat 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 reuse that same value. As a result, JIT-compiled code has fewer interactions with memory and may run faster. Below, we review the class initialization process in the JVM and discuss new rules for strictly-initialized static fields in more depth. We then review the instance initialization process and discuss new rules for strictly-initialized instance fields. This is a preview VM feature, disabled by default The ACC_STRICT_INIT flag denoting a strictly-initialized field is recognized only in class files with a preview version number (XX.65535), and only when preview features are enabled at run time. To enable preview features at run time, use the --enable-preview command-line option: $ java --enable-preview Main Value classes, a new Java language feature, rely upon strict field initialization: Compilers mark all the fields of value classes as ACC_STRICT_INIT. To program with value classes, you must enable preview features at both compile time and run time in order to enable both value classes and strict field initialization. Strict field initialization is a standalone feature in the JVM.
It does not assume that value classes exist, and it can be used by compilers of non-Java languages. Regardless of the compiler, class files with fields marked as ACC_STRICT_INIT can be loaded only if preview features are enabled at run time. Class initialization today Whenever a class is loaded by the JVM, it must be initialized. In bytecode, a class or interface can declare a class initialization method, named <clinit>, for this purpose. The class initialization method is free to execute arbitrary code. Usually, class initialization includes setting all of the class's static fields to appropriate initial values; it may also involve interactions with global state. In Java source code, a class's initialization method is not written directly; it is, rather, an aggregation of the class's static field initializers and static initializer blocks. Each class in a hierarchy may have its own <clinit> method. Every superclass must be initialized before executing the <clinit> method of a subclass. A class whose initialization has begun but not yet completed is considered larval. It is developing, but not yet fully formed. The JVM tracks the initialization state of each class at run time. In today's JVM (see JVMS §5.5), a class's initialization state is one of:
Uninitialized: The class is loaded, but initialization has not yet started.
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 runs while the class is in the 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 is thrown, 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 checks the initialization state of the resolved field's class. If the class is not initialized, but is in the larval state in another thread, then the getstatic instruction blocks until initialization completes.