Enable optimizations in JIT-compilers to constant fold final field loads in generated code.
The goal is to come up with a set of optimizations in JIT-compilers to constant fold final field loads in generated code. During the course of work opportunities for tightening rules for final field updates at run-time will also be explored.
The JVMS and the JMM provide some strong guarantees about final field initialization and visibility.
It's appealing from a performance perspective to exploit them and avoid loading field values which don't change, thus producing more efficient code.
Moreover, optimizations on instance final fields are crucial for performance in
some scenarios. For example, JSR 292 (
java.lang.invoke) heavily relies on
the ability to constant fold loads from final instance fields to get decent
invokedynamic performance (there are special cases in the JVM code for now).
HotSpot JVM already optimizes loads from static final fields, it is
still very conservative when seeing instance final fields. The reason is that
there are scenarios (e.g. deserialization) when the object constructor is skipped
and final field values are written after the object is instantiated.
Immutable objects are promoted as safe in concurrent scenarios and are becoming very popular, so many applications should benefit from such optimizations.
The JVMS is already quite restrictive. At the byte-code level, instance final field
writes are allowed only in constructors (
<init>) and static final field
writes only in static initializers (
However, there's a limited set of additional scenarios when final field updates are possible. There are 4 ways to circumvent the limitations and change a final field value at run-time:
Reflection API (through
Lookup.unreflect()since there's no way to get a setter for a final field)
Unsafe API is deliberately left out of scope. It is designed as a simple,
well-factored set of building blocks to implement low-level JVM operations and
(independently) provide access to some run-time features of the hardware
platform. It is a user's responsibility to ensure that performed operations are
Regarding all other cases, JIT-compilers should take all of them into account when optimizing final field loads and either track updates or be conservative and avoid optimizations.
There are 3 approaches being considered:
tighten run-time rules for final field updates: forbid all stores to final fields once the object is fully constructed;
nullify(ignore and discard) illegal stores to final fields;
track all final field updates in the JVM and adapt accordingly.
The first approach, tightened rules for illegal final field updates, requires the JVM to throw an exception when a store to final field is performed on a properly constructed object (fail-fast approach). It aligns run-time behavior with the JVMS.
<init> byte-code sequence guarantees that the object is
properly constructed once constructor has completed.
It liberates the JVM from the responsibility to track all final field updates and throwing away generated code when a field which was optimized earlier changes its value.
However, there are valid use-cases when JVMS restrictions should be relaxed
(e.g., deserialization). The common scenario is separate object construction
and publication. In such case, the
<init> sequence doesn't work anymore and
non-standard ways to instantiate objects are used. There are 3 ways to create
an instance without running a constructor on it:
ReflectionFactory.newConstructorForSerialization(Class<?>, Constructor<?>)(used by deserialization)
AllocObject(JNIEnv*, jclazz)in JNI
These functions should produce "slushy" objects - objects which can freely change after they are instantiated. The JVM should allow final field updates for such objects and be conservative when optimizing for them.
The "slushiness" property can be recorded as a flag in the object header.
Since it is the user's responsibility to either invoke a constructor or manually initialize the object, an additional operation ("publish"/"freeze") is needed to signal that construction is over and clear the "slushy" flag. It lets the JVM know that the object construction is finished (no more final field updates are planned), so the JVM can harden checks and optimize operations on final fields from then on.
JIT-compilers consult that flag to gate final-folding. Reflection, JNI, and MethodHandles check the flag when attempting to write to a final field and throw a error if it is not set.
The second approach, silently
nullifying stores to final fields in properly constructed
objects, is legal in some cases according to the JMM. Nullification is indistinguishable
from the store occurring but never being observed by a future read. This is
possible if either the store is delayed indefinitely, or if all threads (and
compiled methods) have previously performed a caching read of the original
final value. Additional investigation should be conducted to ensure that the JMM
allows some sort of OOTA caching read of the original final value, since the
threads aren't obliged to physically do such a caching read first.
Finally for the third approach, if there are no adjustments to run-time behavior, the JVM has to track all final field updates and adapt accordingly by invalidating all affected generated code.
java.lang.invoke and the Reflection API should be instrumented with
additional checks to notify the JVM when an application attempts to write to a
final field. The JVM should track all the dependencies in generated code on
final field values.
Risks and Assumptions
There are compatibility risks due to hardened checks in the Reflection API. If an application uses the Reflection API to write final fields, it will get run-time errors when attempting to perform such operations.
External users of
sun.misc.Unsafe are affected if they change final fields in a
properly constructed object. Such updates aren't guaranteed to be visible, i.e. just as today if static final fields are updated.
There's a risk that a user forgets to perform the "publish" operation and the object stays in "slushy" state forever.
That can be mitigated by providing JVM and library diagnostic functionality to detect runaway slushy objects:
The JVM can be equipped with elaborate checks to hunt them down;
sun.misc.Unsafeimplementation which detects final field updates and performs a "slushy" bit check can be implemented.
For a JVM-only optimization, experiments showed a considerable increase in recorded dependencies for generated code during run-time. It stresses dependency tracking machinery in the JVM, both in recording (more space needed) and checking (more work to enumerate affected generated code).
The impact should be measured and additional optimizations considered (e.g. more efficient lookup of per-object dependencies, per-class vs per-object dependency tracking) to reduce both the number of dependencies and dependency tracking overhead.