Stop feeding the loggers! Give more modifiers! Lazy Static Final Fields. Rough sketch features

Original author: John Rose
  • Transfer

It was enough that in Java loggers are initialized at the time of class initialization, why do they litter the entire launch? John Rose to the rescue!


Here is what it might look like:


lazy privatefinalstatic Logger LOGGER = Logger.getLogger("com.foo.Bar");

This document extends the behavior of final variables, allowing you to optionally support lazy execution — both in the language itself and in the JVM. The behavior of the existing lazy evaluation mechanisms is proposed to be improved by changing the granularity: now it will not be accurate to the class, but accurate to the specific variable.



Motivation


Java is deeply embedded lazy calculations. Almost every linking operation can jerk a lazy code. For example, the execution of a method <clinit>(the class initializer bytecode) or the use of the bootstrap method (for invokedynamic call site or constants CONSTANT_Dynamic).


Class initializers are something very rough in the sense of granularity when compared to mechanisms using bootstrap methods, since their contract is to run the entire initialization code for the entire class , instead of limiting initialization to a particular class field. The effects of such a rough initialization are difficult to predict. It is difficult to isolate the side effects of using a single static class field, since computing a single field results in the calculation of all static fields of this class.


If you touch one field, you will touch them all. In AOT compilers, this makes it especially difficult to optimize static field references, even for fields with easily parsed constant values. It should be among the fields to be packed at least one static static field, and it becomes impossible to analyze all fields of this class. A similar problem manifests itself with the previously proposed mechanisms for the implementation of constant convolution (during javac operation ) for constant fields with complex initializers.


An example of a re-initialized field initialization, which occurs in different projects at every step, in each file is the initialization of the logger.


privatefinalstatic Logger LOGGER = Logger.getLogger("com.foo.Bar");

This innocuous-looking initialization launches a tremendous job under the hood that will be performed during class initialization - and yet it is extremely unlikely that a logger is really needed at the time of class initialization, and maybe not needed at all. The ability to postpone its creation until the first real use will simplify initialization, and in some cases will allow this initialization to be avoided altogether.


Final variables are very useful, they are the main mechanism of the Java API in order to indicate the constancy of values. Lazy variables have also proven themselves. Starting with Java 7, they began to play an increasingly important role in the internals of the JDK, being annotated @Stable. JIT can optimize both final and “stable” - variables are much better than just some variables. Adding lazy final variables will allow this useful usage pattern to become more common, will enable to be used in more places. Finally, using lazy final variables will allow libraries, such as the JDK, to reduce code dependency <clinit>, which, in turn, should reduce startup time and improve the quality of AOT optimizations.


Description


The field can be declared with a new modifier lazy, which is a contextual keyword, perceived solely as a modifier. This field is called lazy ( lazy field ), and must also have modifiers staticand final.


The lazy field must have an initializer. The compiler and runtime agree to launch the initializer exactly when the variable is first used, and not during the initialization of the class to which this field belongs.


Each lazy static finalfield is associated at compile time with a constant pool element that represents its value. Since the constant pool elements themselves are lazily calculated, it is sufficient to simply assign a correctly selected value for each static lazy final variable associated with this element. (More than one lazy variable can be attached to one element, but it is unlikely to be a useful or meaningful feature.) The attribute name is LazyValue, and it must refer to a constant floor element that can be ldc-stitched to a value that is convertible to a lazy field type. Only those casts that are already used in are allowed MethodHandle.invoke.


Thus, a lazy static field can be viewed as a named alias to a constant pool member within the class that declared the field. Tools like compilers can in some way try to use this field.


A lazy field is never a constant variable (in the sense of JLS 4.12.4) and is explicitly excluded from participation in a constant expression (in the sense of JLS 15.28). Therefore, it never captures an attribute ConstantValue, even if its initializer is a constant expression. Instead, a lazy field captures a new kind of attribute of a classfile called LazyValueJVM with which it is checked when linking to that particular field. The format of this new attribute is similar to the previous one, because it also points to the constant pool element, in this case, the one that resolves to the field value.


When a lazy static field is linked, the normal execution process of class initializers should not disappear. Instead, any method of the <clinit>declaring class is initialized according to the rules defined in JVMS 5.5. In other words, the bytecode getstaticfor a lazy static field performs the same linking as for any static field. After initialization (or during the already-started initialization of the current thread), the JVM resolves the constant pool elements associated with the field, and stores the values ​​obtained from the constant pool into this same field.


Since lazy static final cannot be empty, they cannot be assigned any values ​​— even in the small number of contexts where it works for empty final variables.


During compilation, all lazy static fields are initialized independently of non-lazy static fields, regardless of their location in the source code. Hence, restrictions on the location of static fields do not apply to lazy static fields. An initializer of a lazy static field can use any static field of the same class, regardless of the order in which they occur in the source. An initializer of any non-static field or a class initializer can refer to a lazy field, regardless of the order in which they are relative to each other in the source code. Usually, doing this is not the most sensible idea, because it loses the whole meaning of lazy values, but perhaps this can somehow be used in conditional expressions or on a control flow.


Lazy fields can be detected using the reflection API using two new API methods in java.lang.reflect.Field. A new method isLazyreturns trueif and only if the field has a modifier lazy. A new method isAssignedreturns falseif and only if the field is lazy and still not initialized at the time of launch isAssigned. (It can return true almost on the next call in the same thread, depending on the availability of races). There is no way to tell if a field is initialized, except with isAssigned.


(The call is isAssignedneeded only to help with the rare problems associated with resolving cyclic dependencies. Perhaps we can do without implementing this method. However, people who write code with lazy variables sometimes want to know neatly, whether it is set to such a variable value or not yet, in approximately the same way as users of mutexes sometimes want to learn from a mutex, whether it is blocked or not, but they don’t really want to be blocked)


There is one unusual restriction on lazy final fields: they should never be initialized to their default values. That is, the lazy reference field should not be initialized to null, and numeric types should not have a null value. A lazy boolean value can be initialized with just one value - truebecause it falseis its default value. If the initializer of a lazy static field returns its default value, the linking of this field will drop with the corresponding error.


This restriction is introduced in order. to allow JVM implementations to reserve defaults as an internal watchdog value marking the state of an uninitialized field. The default value is already set in the initial value of any field, set at the time of preparation (this is described in JLS 5.4.2). So this value naturally already exists at the beginning of the life cycle of any field, and therefore is a logical choice for use as a watchdog value that tracks the state of this field. Using these rules, the initial default value can never be obtained from a lazy static field. For this, the JVM can, for example, implement a lazy field as an immutable reference to the corresponding element of the constant pool.


Restrictions on the default values ​​can be circumvented by wrapping the values ​​(which are possibly equal to the default ones) in boxes or containers of some convenient type. Zero number can be wrapped in a non-zero reference to Integer. Non-primitive types can be wrapped in Optional, which becomes empty if it hits null.


To support freedom in the way of implementing features, the requirements for the method are isAssignedspecially underestimated. If the JVM can prove that a lazy static variable can be initialized without observable external effects, it can do this initialization at any time. In this case, isAssignedwill return trueeven if getfieldnever called. It isAssignedonly imposes the requirement that if it returns false, then none of the side effects of variable initialization should be observed in the current thread. And if he returns true, then the current thread may in the future observe the side effects of initialization. Such a contract allows the compiler to substitute ldcforgetstatic for eigenfields, which allows the JVM to avoid tracking detailed states of final variables that have common or degenerate elements in a constant pool.


Several threads can enter the race for initializing the lazy final field. As it already happens with CONSTANT_Dynamic, the JVM selects an arbitrary winner of this race and provides the value of this winner to all the threads involved in the race, and writes it down for all subsequent attempts to get the value. To get around the race, specific JVM implementations can try to use CAS operations, if the platform supports them - the race winner will see the previous default value, and the losers will see the non-default value that won the race.


Thus, the existing rules for the single assignment of final variables continue to work and now capture all the complexities of lazy calculations.


The same logic applies to secure publishing using final fields — it is the same for both lazy and non-lazy fields.


Note that a class can convert a static field to a lazy static one without violating binary compatibility. The client instruction is getstaticidentical in both cases. When a variable declaration changes to lazy, it is getstaticlinked in another way.


Alternative solutions


You can use nested classes as containers for lazy variables.


You can define something like a library API to manage lazy values ​​or (more generally) any monotonic data.


Refactor what the lazy static variables were going to do so that they turned into nullar static methods and their bodies were published using ldc CONSTANT_Dynamic constants in some way.


(Note. The workarounds above do not provide a binary-compatible way to evolutionally untie existing static constants from their tie to <clinit>)


If we talk about providing more functionality, you can allow lazy fields to be non-static or non-final, while maintaining the current correspondences and analogies between the behavior of static and non-static fields. A constant pool cannot be a repository for non-static fields, but it can still hold bootstrap methods (depending on the current instance). Frozen arrays (if implemented) can get the lazy option. Such studies are a good basis for future projects based on this document. And by the way, such opportunities make even more meaningful our decision to ban default values.


Lazy variables must be initialized with their own initializing expressions. Sometimes this seems like a very unpleasant limitation, which throws us back into the days of inventing empty final variables. Recall that these empty final variables can be initialized by arbitrary blocks of code, including the try-finally logic, and they can be initialized in groups rather than simultaneously. In the future, it will be possible to try to apply the same possibilities to the lazy final variables. Perhaps one or more lazy variables can be associated with a private block of initialization code, the task of which is to assign each variable exactly once, as it happens with a class initializer or an object constructor. The architecture of such a feature may become clearer after the appearance of deconstructors,


Minute advertising. The Joker 2018 conference will take place very soon, featuring many prominent Java and JVM specialists. View the full list of speakers and reports on the official website .

Author


John Rose is a JVM engineer and architect at Oracle. Lead Engineer Da Vinci Machine Project (part of OpenJDK). Lead Engineer JSR 292 (Supporting Dynamically Typed Languages ​​on the Java Platform), deals with the specification of dynamic calls and related issues, such as type profiling and advanced compiler optimizations. Previously, he worked on inner classes, did the original HotSpot port on SPARC, the Unsafe API, and also developed many dynamic, parallel, and hybrid languages, including Common Lisp, Scheme ("esh"), dynamic binding for C ++.


Translator


Oleg Chirukhin - at the time of this writing, he works as a community manager in the JUG.ru Group company, and is involved in the popularization of the Java platform. Before joining JRG, he participated in the development of banking and state information systems, the ecosystem of self-written programming languages, and online games. Current research interests include virtual machines, compilers, and programming languages.


Also popular now: