JDK-8209964 : Lazy Static Final Fields
  • Type: JEP
  • Component: tools
  • Priority: P3
  • Status: Draft
  • Resolution: Unresolved
  • Submitted: 2018-08-25
  • Updated: 2023-04-27
Related Reports
Blocks :  
Relates :  
Description
<!-- 0.1 Apr 29 2014; 0.2 July 31 2018; 0.3 December 13 2019; 0.4 June 3 2022; 0.5 October 24 2022 -->

Summary
-------

Expand the behavior of final variables to include optional lazy
evaluation patterns, in language and JVM.  In doing so, extend Java's
pre-existing lazy evaluation mechanisms to per-variable granularity,
from its current per-class granularity.

Motivation
----------

Java uses lazy evaluation pervasively.  Almost every linkage operation
potentially triggers a lazy evaluation, such as the execution of a
`<clinit>` method (class initializer bytecode) or invocation of a
bootstrap method (for an `invokedynamic` call site or
`CONSTANT_Dynamic` constant).

Class initializers are coarse-grained compared to mechanisms using
bootstrap methods, because their contract is to run *all*
initialization code for a *whole* class, rather than some
initialization that may pertain to a particular field of that class.
Such coarse-grained initialization effects make it especially
difficult to predict and isolate the side effects of using *one*
static field from the class, since computing the value of one field
entails computation of *all* static fields in the same class.

So touching one field touches them all.  In AOT compilers, this makes
it difficult to optimize a static field reference, even if the field
has a clearly analyzable constant value.  It only takes *one*
extra-complicated static field in a class to make *all* fields
non-optimizable.  A similar problem appears with proposed mechanisms
for constant-folding (at `javac` time) constant fields with complex
initializers.

As an example of an extra-complicated static field initialization,
which in some codebases appears in almost every file, consider logger
initialization:

    private static final Logger LOGGER = Logger.getLogger("com.foo.Bar");

This harmless-looking initialization triggers a tremendous amount of
behind-the-scenes activity at class initialization time – though it
is unlikely that the logger is needed at class initialization time, or
even at all.  Deferring the creation to first use would streamline
initialization, and might result in optimizing away the initialization
entirely.

Final variables are very useful; they are the main mechanism for Java
APIs to denote constant values.  Lazy variables are also well-proven.
Since Java 7 they have been an increasingly important part of JDK
internals, expressed via the internal `@Stable` annotation.  The JIT
can optimize both final and "stable" variables more fully than other
variables.  Adding lazy finals will these useful design patterns
usable in more places.  Finally, their adoption will allow libraries
such as the JDK to downsize their reliance on `<clinit>` code, with
likely improvement to startup and AOT optimizations.

Non-Goals
---------

It is not a goal to change the existing rules for initializing regular
static final fields, since such a thing cannot be done without subtly
breaking many kinds of user code.  This feature will be provided to
users on an opt-in basis.  We hope IDEs will assist users in making
use of it, by detecting and peforming refactorizations that would
benefit from increased laziness.

Many of the advantages of lazy evaluation mentioned in this JEP could
apply to a corresponding feature which allows lazy instance variables,
that is, fields which are not static.  This is left as a possibility
for the future.  The encoding of the uninitialized state will require
extra care to avoid excessive latency and footprint costs, since the
encoding must be replicated independently across all instances of the
object.

Some of the advantages of lazy evaluation mentioned in this JEP could
conceivably apply to a corresponding feature which allows non-final
fields to be lazy, that is, fields which can be assigned to by user
code as well as be read by user code, either "at speed" or varying
slowly as some global "epoch" evolves.  Such ideas are left as
possibilities for the future.

It is not a goal to define "super-eager" statics which can be
initialized before their declaring class runs its initializer method.
Such statics can be simulated by hand, using a separate class in a
[holder idiom].  However, it is expected that many lazies, once
decoupled from co-initialization with their siblings, will (upon
further analysis) show that they can be shifted in both directions,
just as today's constant expressions can be shifted.

[holder idiom]: <https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom>

Description
-----------

A field may be declared with a new modifier `lazy`, a contextual
keyword recognized only as a modifier.  Such a field is called a *lazy
field*, and must also be static and final.

> (In other accounts of this idea, lazy statics are marked using other
variations of modifier syntax, such as `__LazyStatic` or
`lazy-static`.  Details are TBD.)

A lazy static field must be supplied with an initializer.  The compiler and
runtime arrange to execute the initializer on the first use of the
variable, not when the containing scope (the class) is initialized.

> (The initialization of a lazy static field, being handled by this
special mechannism, is therefore not also present in any `<clinit>`
method generated by the compiler.)

Each lazy static final field is associated at compile time with a
constant pool entry which supplies its value.  Since constant pool
entries are themselves lazily computed, this is sufficient to assign a
well-defined value to any static lazy final variable associated with
the constant pool entry.  The name of the attribute is `LazyValue`, and it
must refer to a constant pool entry that can be `ldc`-ed to a value
that can be converted to the type of the lazy field.  The allowed
conversions are the same as those used by `MethodHandle.invoke`.

> (In principle more than one lazy variable could be associated with a
single constant pool entry, although this is not envisioned as a
useful feature.  Such grouping patterns can be built by hand on top of
the basic non-grouped language feature.)

Thus, a lazy static field may be viewed as a named alias of a constant
pool entry within the class that defined the field.  Tools such as
compilers may exploit this property.

A lazy static field is never a constant variable (in the sense of JLS 4.12.4)
and is explicitly excluded from contributing to a constant expression
(in the sense of JLS 15.28).  Thus, it never possesses a
`ConstantValue` attribute, even if its initializer is a constant
expression.  Instead, a lazy field possesses a new kind of classfile attribute
called `LazyValue`, which the JVM consults when linking a reference to
that particular field.  The format of this new attribute is similar to
the old one, because it also points to a constant pool entry, in this
case the one which resolves the field value.

When linking a lazy static field, the normal process of executing
class initializers is *not* bypassed.  Instead, any `<clinit>` method
on the declaring class is executed according to the rules of JVMS
5.5.  In other words, a `getstatic` bytecode of a lazy static field
performs any linkage actions associated with *any* static field,
except the lazy ones.

> (When a class is initialized due to linkage of a normal static
field, the class's initialization at that time does not involve lazy
static fields in any way; it is as if they are not declared, precisely
because they are not initialized by the actions of any `<clinit>`
block.)

After initialization (or during an already-started initialization in
the current thread), the JVM then resolves the constant pool entry
associated with the field, and stores the value of that constant
pool entry into that field.

Since lazy static final fields cannot be blank finals, they cannot
be assigned to, even in those limited contexts where blank finals
may be assigned to.

There is a rule in Java which requires that a static variable may only
appear in the initializers of static variables which occur later on in
the class body.  This rule reduces (but does not eliminate) the
possibility that an untimely read of a static variable may obtain
the default value of that varaible, rather than its initial value.

    class C {
      static int x = y; //error: illegal forward reference
      static int y = 42;
    }

These ordering constraints are observed even for lazy static fields,
as if they were not declared lazy.  Thus, a lazy static field's
initializer can only refer to a static field of the same class that
occurs earlier in the same source file.

If in some case two lazy values must depend on each other in a
circular relationship, the cycle can be hidden by the use of a private
static method.  In that case, a true cyclic dependency will cause a
stack overflow error.  In the case of non-lazy statics, an analogous
cycle would cause a default value to become visible.

    class C {
      //lazy static final Object x = y, y = x; //error
      lazy static final Object x = ycycle(), y = x;
      private static Object ycycle() { return y; }
    }

Any non-lazy static field initializer or class initializer
block may also refer to a lazy static field value that precedes
in the the source file.  This is usually not desirable, as it would
tend to cancel the benefit of the lazy field, but may be useful in
combination with conditional expressions or control flow.

The purpose of the ordering rule is to require the user to specify a
nominal initialization order for lazy statics.  The actual dynamic
initialization order may differ, but the nominal order serves to
demonstrate statically that there are no unintentional cyclic
dependencies between the statics, lazy and otherwise.

Lazy fields may be recognized by the core reflection API by use of two
new API points on `java.lang.reflect.Field`.  The new query method
`isLazy` returns `true` if and only if the field was declared lazy.
The new query method `isAssigned` returns `false` if and only if the
field is lazy and has not been initialized, at the moment the method
is called.  (It may return `true` on the very next call in the same
thread, depending on race conditions.)  Other than `isAssigned`, there
is no way to observe whether a lazy field has been initialized yet.

(The `isAssigned` reflective call is provided only to assist with
occasional problems with circular initialization dependencies.
Perhaps we can get away without implementing it, although people who
code with lazy variables occasionally want to ask gently whether a
lazy variable is set yet, in the same way that users of mutexes
occasionally want to ask whether a mutex is locked, but without
actually seizing the lock.)

To preserve implementation freedom, the contract of `isAssigned` is
minimized.  If a JVM can prove that a lazy static variable can be
initialized without observable side effects, it may do so at any time;
in such a case the `isAssigned` query will report `true` even before
any `getfield` is executed.  The minimized contract for `isAssigned`
is that if it returns `false`, none of the side effects from
initializing that variable have yet been observed by the current
thread, whereas if it returns `true`, then the current thread can, in
the future, observe all side effects of initialization.  This contract
allows compilers to substitute `ldc` for `getstatic` of their own
fields, and allows JVMs to avoid tracking detailed initialization
states of finals with shared or degenerate constant pool entries.

Multiple threads may race to initialize a lazy final.  As is already
the case with `CONSTANT_Dynamic` constant pool entries, the JVM picks
an arbitrary winner of such a race and provides the value from that
winner to all racing threads, as well as recording it for all future
accesses.  Thus, JVM implementations may elect to use CAS operations,
if the platform supports those, to resolve races.

When the JVM stores a value into a lazy final field, it performs a
_freeze_ operation.  This freeze happens before any `getstatic`
instruction is allowed to see the field value.  This is how
pre-existing rules for safe publication apply to lazy finals.

The effect of a lazy final is closely similar to the effect of a
static final defined on its own class, with no other static finals.

    class C { lazy static final Object x = xval(), y = yval(); }
    f() { ... getstatic C.x ... }
    =>
    class C_x { static final Object x = xval(); }
    class C_y { static final Object y = yval(); }
    f() { ... getstatic C_x.x ... }

The difference is that a true cyclic dependency between lazy statics
will cause a stack overflow, rather than the observation of a default
value.

Note that a class can convert a static to a lazy static without
breaking binary compatibility.  A client's `getstatic` instruction
is identical in both cases.  When the variable's declaration changes
to lazy, then the `getstatic` instruction links differently.

### Operational Description

A class with no lazy static fields is initialized in one pass.  A
class with N lazy static fields is initialized in 1+N passes.  The
first pass always initializes all of the normal static fields (and not
the lazy static fields), in the order they occur in the source code,
as in all prior versions of the Java Language Specificaiton.  All of
the passes are initiated by threads which are attempting to access the
class, but the passes may also be run in distinct threads.

The first pass is run to completion in a thread chosen from among
those threads which are first to perform an initializing access (of
any kind) to the class.  Racing threads are paused until the chosen
thread completes the initialization.  The race is arbitrated by
locking on the initialization lock "LC" defined in [JVMS 5.5],
which is the place where class initialization is defined.

[JVMS 5.5]: <https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-5.html#jvms-5.5>

If the initializing thread reads a normal static before that same
thread has initialized it, the default initial value, such as `null`,
will be seen as the value of that field.

If the initialization of any normal static variable fails with an
exception, then the initialization of the class as a whole fails.
An `Error` (possibly an instance of `ExceptionInInitializerError`) is
recorded to diagnose this failure, as specified in step 11 of [JVMS
5.5].

Each other pass (for a lazy static) is run to completion in a thread
which is chosen from among those threads which are first to perform an
initializing access to that lazy static.  The pass evaluates the
initializer of the lazy static and produces either a correctly
typed initial value for that lazy static, or throws an exception.

If an exception is thrown, then all uses of the lazy static, including
the first and those of any threads racing to initialize, are reported
via an `Error` which is thrown in all racing threads, again as
specified in step 11 of [JVMS 5.5].  As with class initializer errors,
the same error is recorded and will be thrown for future access
attempts to the same lazy static.  Unlike normal statics,
initialization failure does not cause the class as a whole to fail to
initialize.

> (Unless the initializer returns the default value of that field's
type, no use of that lazy static will see the default value of that
field's type.  This behavior is unlike that of normal statics.)

Exactly as with non-lazy class initialization, races between threads
to initialize a lazy static are arbitrated by locking on the
initialization lock "LC".  This means that, for any given class,
initialization expressions for statics are never executed concurrently
with respect to each other.  It also means that if a class initializer
should refer to a lazy static, the initializer of that lazy static is
guaranteed to be resolved in the same thread, and this resolution will
complete before the class initializer completes.

If an initializing thread reads, directly or indirectly, the value of
the lazy static before the initialization value has been chosen, a
stack overflow error will be thrown instead of completing the
initialization.  If that thread is in fact the one chosen to
initialize the lazy static, then that error will be the result
of reading the lazy static in all threads at all times.

> (Because the lock "LC" arbitrates all lazies in one class, deadlock
is impossible; this is an advantage over schemes which rely on
multiple classes and can deadlock if initialization starts on
different classes in different threads.)

> (The initialization rules for lazy statics are aligned with those of
normal statics, and thus are necessarily distinct from the rules for
`CONSTANT_Dynamic` constants in constant pools.  Serializing on "LC"
and wrapping errors in `ExceptionInInitializerError`, as required by
lazy statics, are implemented not by the JVM but by the translation
strategy, presumably in a carefully-coded bootstrap method.)

If the first field in a class that is linked is a lazy static field,
then the first pass initializes all the normal static fields.  Next,
the lazy static field is initialized.  If several threads are
attempting to initialize the same lazy static field, one of them is
chosen to initialize all of the normal static fields while all others
are paused.  After that pass, in a second pass, another thread is
chosen (by arbitrarily on "LC") to compute the initializer, and the
result (either a value or an exception) is recorded for the lazy
static.

> (It is typical but not required that the same thread will perform
both passes.  The initialization of normal statics can be ordered
freely relative to the initialization of any lazy static, as long as
initialization of normal statics has commenced before the
initialization of any lazy static is commenced.)

> (If some future system allows statics to be initialized before the
`main` method is entered, then these ordering rules will still apply,
which means, for some classes, all normal statics and *some* lazy
statics may be initialized before the invocation of the `main` method.
In such a system, it may be desirable to convert of normal statics of
a class to lazy statics, to remove the bottleneck of a `<clinit>`
method.)

Subsequently, linking any of the other N-1 lazy static fields will
initialize just that lazy static field, again by choosing a requesting
thread to produce either a value or an exception.

If all the lazy static fields in the class are eventually linked, then
the number of passes to fully initialize the class is 1+N.

> (Either way, all the normal static fields are initialized in the
first pass. This "eager" initialization of normal static fields when
the first static field is linked is consistent with prior releases of
Java.)

Alternatives
------------

Use nested classes in a [holder idiom] for single lazy variables.
This has worked in the past for small numbers of variables, but does
not scale up well enough to allow large numbers of static finals to be
refactored as lazy.  (The code churn alone would be prohibitive, as
would the costs of loading many new small classes.)  But such
large-scale refactoring appears to be desirable, in order to improve
the scope and quality of static analyses made by Leyden condensation
transforms, at least those which propose to reorder the effects of
class initialization.

Define some sort of library API for managing lazy values or (more
generally) monotonic data.  This would suffer also from scaling
problems.

Refactor would-be lazy static variables as nullary static methods and
populate their bodies with `ldc` of `CONSTANT_Dynamic` constants, by
some means.  This has a common problem with the holder idiom, since it
often requires APIs to be changed, as well as significant code churn.

Use non-final variables for publication of lazily evaluated data,
being careful not to modify them, and to fence their initialization
for safe publication.  (Same objections as the holder idiom, plus
increased exposure to coding errors, with the fencing.)

(N.B. The above workarounds do not provide a binary-compatible way
to evolve existing static constants away from their current reliance
on `<clinit>`.  For non-private statics, this is a problem in addition
to code churn and footprint.)

In the direction of adding *more* functionality, we could allow lazy
fields to be non-static and/or non-final, preserving current
correspondences and analogies between static and non-static field
behaviors.  The constant pool cannot be a backing store for non-static
fields, but it can still contribute bootstrap methods (that depend on
the current instance).  Frozen arrays (if implemented) could be given
lazy variations, perhaps.  Such investigations seem plausible as a
follow-on projects for the current proposal.  (See Non-Goals also.)

### Sketch of Translation Strategy

Given the simple "hook" of the `LazyValue` attribute, there are a
number of possible tactics for translating lazy statics to bytecodes.
Here is one combination that seems good.

  - For each lazy static initializer, the Java compiler (e.g., `javac`)
    collects a separate block of code which contains a `return`
    statement that returns the initializer value, possibly boxed as an
    `Object`.

  - All such a blocks are combined into a `switch` statement, where
    the target is a string and the case labels are the names of the
    fields.  (Note that they must all be distinct from each other.)

  - The switch statement is packaged in a synthetic static method
    called `$lazyinit`, taking one string argument, returning an
    untyped object reference, and throwing an arbitrary set of
    exceptions.

  - A single `CONSTANT_Dynamic` entry is emitted which takes a
    reference to `$lazyinit` (as a method handle) and, via an
    appropriate bootstrap method (in `java.lang.runtime`) spins up an
    object or method handle which "knows how" to accept a lazy static
    name string and arbitrary the lazy initialization process, as
    described above.  (The details depend on a possibly adjusted
    JVMS.)  Call this the "lazy initialization controller constant".

  - A `CONSTANT_Dynamic` entry is emitted for each lazy static field.
    The name and type of the entry are copied from that of the field
    itself.  The bootstrap method and/or arguments link to the
    previously defined lazy initialization controller constant.
    (The details depend on a possibly adjusted JVMS.)

  - A `LazyValue` attribute is emitted for each lazy static field,
    which points at the corresponding `CONSTANT_Dynamic` entry.

The overhead of this scheme for N lazy static fields is:

  - N blocks of initialization code, each with a return as `Object`.
    (Comparable to code for non-lazy initalization, which ends with
    a `putstatic` of the correct type.)

  - One synthetic method containing a string-switch, with its
    metadata.  (Roughly 100 bytes.)

  - Exactly two bootstrap method items in the constant pool.

  - N+1 `CONSTANT_Dynamic` items in the constant pool.

  - N `CONSTANT_NameAndType` items in the constant pool.  (Identical
    to those required for `getstatic` or `putstatic`, so no extra
    cost.)

The incremental cost of a lazy static can thus be reduced to just a
few bytes in a classfile.  The runtime spinup and footprint costs are
another matter which depends sensitively on whether the JVMS supports
the implementation of "LC" arbitration directly on top of the constant
pool, or whether JDK code must duplicate the states required for such
arbitration.  This is being investigated further, in terms of an
extension (TBD) to the bootstrap protocol that would expose a wider
range of constant pool states to the bootstrap method.

Comments
I have a prototype which approximates the desired semantics, with some simulation costs. As part of the "Sketch of Translation Strategy" above, the BSM looks like the code below. I have a proposal (forthcoming) for how to tune the BSM protocol to make this more efficient. // Sketch of "manager object" instantiated once per class, // if that class contains one or more lazy statics. class LazyStaticManager { … Object runLazyStaticInitializerExactlyOnce(String name) { synchronized (this.initializationLock) { ResultOrError done = state.get(name); if (done == PENDING) { throw new StackOverflowError("init cycle detected"); } if (done == null) { this.state.put(name, PENDING); // cycle detection Object result = null; Error error = null; try { // Call back to the method baked into the class file. result = this.lazyinit.invokeExact(name, SENTINEL); } catch (Throwable ex) { error = (ex instanceof Error) ? (Error) ex : new ExceptionInInitializerError(ex); } if (result == SENTINEL) { throw new IllegalArgumentException("wrong name"); } done = new ResultOrError(result, error); this.state.put(name, done); } if (done.error != null) { throw done.error; } else { return done.result; } } } } Weaknesses of this scheme: - this.initializationLock is private to JVM, hard to know - this.state table duplicates constant pool states, expensively To fix these, just allow the LazyStaticManager object more direct access to the private lock and to the constant pool states, using a private “unsafe-ish” API, then allow that API to query and set those states under the VM’s lock. This may require an enhancement to JVMS-defined BSM protocols. A prototype is here (please ignore the noise from auto-vs-lazy name bikeshedding): https://github.com/rose00/jdk/blob/dec004e11517/src/java.base/share/classes/java/lang/runtime/AutoStaticManager.java#L92
16-11-2022

I prefer this semantics but i'm not sure it can be only specified as a BSM of a constant dynamic without providing a new API for it. Currently the BSM of a condy is not able to read and write at the location of the constant in the runtime mirror of the constant pool, you just send the value and hope for the best, so the read/write can not be done under a lock.
25-10-2022

I adjusted the rules to make lazy statics serialize just like <clinit>, and throw ExceptionInInitializerError. This makes them much more like regular statics, which should make them easier to adopt. The cost of this is extra "shim logic", specified by the translation strategy in the bootstrap method for the condy that manages the static; this is sketched. The changes to the JVMS will continue to be minimized. For a current "hot take" please see: http://cr.openjdk.java.net/~jrose/pres/202210-Lazy-Static.pdf
24-10-2022

I removed the restriction about default values, and dialed back discussion of non-static lazies. The reduced proposal should be simpler to adopt.
13-12-2019