final field values should be trusted as constant (filed as JDK-8233873)

John Rose john.r.rose at oracle.com
Sat Nov 9 01:24:10 UTC 2019


https://bugs.openjdk.java.net/browse/JDK-8233873

# Problem

The JVM JITs routinely optimize references to final fields as constant
values, when a JIT can deduce a constant containing object.  This is a
fundamental capability for producing good code.

Currently, though, only a small number of "white listed" fields are
treated in this way, since vigorously optimizing _all_ final fields is
thought to have unknown risky consequences.  The white listing logic
is defined using the function `trust_final_non_static_fields` and
similar logic setting `
as part of changes like JDK-6912065 and JDK-8140483.

# Proposal

The JVM should support an option `FoldConstantFields` which treats
bypasses the above "white list" and uses a "black list" instead as
needed.  Initially this option should be turned off by default.
Turning it on should, initially, also turn on a new option
`VerifyConstantFields` which detects updates to final fields and
diagnoses them with some selectable mix of warnings or errors.

(See below for discussion of how updates to final fields can occcur.
The short summary is "reflection, JNI, or Unsafe". Each of these
requires a different remediation.)

This feature will not solve the problem of full optimization of
constant fields all at once, but will set the stage for finding and
fixing problems caused by such optimizations.

The support for `FoldConstantFields` should include (either initially
or as follow-on work) the following functions:

 - Dependency recording in the JIT, whenever a final field value is
   used.  At first this should be recorded per field declaration, not
   per individual field instance, on the assumption that invalidation
   will be very rare.  This assumption may need to be revised.

 - Updates to final fields via reflection must be trapped and must
   trigger deoptimization of dependent JIT.

 - Updates to final fields via JNI must be trapped similarly.

 - Updates to final fields via other users of `Unsafe` must be trapped
   similarly.  This addresses uses of `Unsafe` _that the JDK knows
   about and controls_.

 - Encourage other users of `Unsafe` to perform similar notifications,
   and document how to do so.  Perhaps there are additional `Unsafe`
   API points to notify the JIT.

 - Placing the checking logic inside `Unsafe` is the wrong answer in
   most cases, since it would penalize well-behaved users of `Unsafe`.
   Perhaps a separate flag `VerifyUnsafeUpdates` would be applicable,
   for stress tests where performance can be sacrificed.

 - Define an API for use by privileged frameworks (including those in
   the JDK) for creating objects in a "larval" state, apart from
   normal constructor invocation.  (Possibly `Unsafe.allocateInstance`
   is such an API point; see also JNI AllocObject.)  These are
   released from the constraints on final field writing, including JIT
   invalidation.  If a JIT encounters an object in the larval state,
   the JIT will simply refrain from constant-folding its fields.

 - Define an API for promoting larval objects to a normal "adult"
   state, at which point the normal JIT optimizations would apply.  If
   this isn't done, performance will be lost only regarding the larval
   objects created by old frameworks, so perhaps this isn't needed.

 - It seems likely that the larval and adult states would need to be
   reflected in a bit pattern in the object header.  As an
   optimization, normally constructed objects would probably not need
   to have this state change in their header bits, unless perhaps they
   "escape" during their constructor call.

# Discussion

A final field can in some cases be assigned a new value.  If a JIT has
already observed the previous value of that final field, and
incorporated it into object code as a constant, then (after the
assignment of a new value to that field), the optimized object code
will execute wrongly.  We call such wrongly executing code "invalid",
and the JVM takes great care to avoid executing invalid code in
similar cases involving speculative optimizations, such as
devirtualized method calls or uncommon traps.

The basic reason for this is that the Java Memory Model requires that
all fields (including changed final fields) must be read accurately.
An accurate read yields a value that is appropriate to the current
thread, as defined by a web of "happens-before" relations.  (It is not
entirely wrong to think of these relations as a linear set, although
concurrency and races are also part of the JMM.)

But field fields _must_ be changed when an object is initialized, and
_may rarely_ change in other circumstances.  There are a number of
ways to change the current value of a final field:

0. In a constructor, a final field may be changed from its current
value (typically initial default value) to a new (possibly
non-default) value.  The JVM (per specification) allows this to occur
_multiple times_ although most sources of bytecode are thought to
avoid such behavior.

1. When a field is reflected, and `setAccessible(true)` is called, the
value may be set.  This "hook" is intended for use by deserializers
and other low-level facilities.  It is thought to be used as a
simulation of case #0 above, when an object's constructor cannot be
conveniently invoked. In a real sense, holding this option open for
serialization frameworks harms the optimization of the entire
ecosystem.

2. JNI functions such as SetBooleanField can be used to smash new
values into fields even if they are final.

3. Good old `Unsafe.setInt` can be also be used to smash new values
into fields (or parts of fields or groups of fields) even if they are
final.

Although a debugger can forcibly change the value of a field from
outside the JVM, via APIs in the `jdk.jdi` module, it appears to be
impossible to use those APIs to change final fields.

It is unknown what libraries or bytecode spinners "in the wild" are
using any of the four options above in ways that would invalidate
JIT-compiled code.  Setting the JITs free to optimize fully requires a
plan for mitigating the impact of final field changes both in known
code (in the JDK) and in unknown "wild" code.

## Side note on races

Although race conditions (on non-volatile fields) allow the JVM some
latitute to return "stale" values for field references, such latitude
would usually be quite narrow, since an execution of the invalid
optimized method is likely to occur downstream of the invalidating
field update (as determined by the happens-before relation of the
JMM).  The JMM itself would have to be updated to either relax
happens-before relations pertaining to final field updates, or else
allow special race conditions that allow the JIT to use stale values
of final fields (in effect, loo king backward in time, past events
visible through the relevant happens-before events).  There are no
active proposals to update the JMM in this way, and it seems easier to
take the JMM as a given, or (at most) make very small changes to it to
further specialize the treatment of final fields.



More information about the hotspot-compiler-dev mailing list