javajitfinal

How are final objects in Java handled during JIT compilation?


In the GO language, constant values must be determined at compile time. In Java, when creating an object, you can initialize the final constant value by passing the value as a constructor parameter. This means that the value is determined at runtime rather than at compile time. So, I am curious about how the value of final, which is dynamically initialized depending on the object, is compiled when compiled with JIT.

chatGPT responded that dynamically initialized final objects are treated as variables, not constants, by the JIT compiler.


Solution

  • An instance final field is just a field that cannot be changed after object initialisation, so to a compiler it does not mean much, unless the object itself is constant.

    A static final field, on the other hand, does not depend on a holding object. As a result, it is treated as a constant from the perspective of the JIT compiler. You may ask, what if the field has not been initialised yet. In that case, the JIT compiler can bail out to wait for the holder class to be initialised, there are corner cases such as when the compiled method is the <clinit> that is initialising the static final fields but you will rarely meet those. The logic can be found here.

    As a result, a static final primitive can be treated as a constant. Things get more complicated when it comes to non-primitive fields, in those cases, while the object is constant*, its content may not. This will generally be the case in the foreseeable future, as the program is permitted to modify even an instance final variable, which means we cannot safely constant-fold those**.

    However, due to strong encapsulation, modules are generally not permitted to lurk into the implementation details of other modules. As a result, the compiler can special-case some known classes that it can trust to not change their final fields to improve performance. Records are also trusted since they are new and was designed with optimality in mind. Additionally, the JDK has an internal annotation @Stable that it uses to additionally mark immutable fields, this annotation has additional benefits that it guarantees array elements to not change instead of only the array identity. This has several implications:

    In conclusion, a static final field is generally a constant, and a field of a constant is generally not constant itself unless it belongs to a class that is a record, a hidden class, or a class that is treated specially by the compiler.

    * This means that changing a static final field is an undefined behaviour (this is undefined in the mean that you are violating the compiler assumption regarding the program behaviour, which may lead to wrong results, impossible code execution, crashing the VM or wipe out your hard drive). However, the only exception is the fields of java.lang.System, which are specified to be mutable.

    ** Actually the specification do not allow modification of instance final fields, but there are a lot of programs out there relying on this ability, most notable are the serialisation libraries. You can use -XX:+TrustFinalNonStaticFields to force the compiler to trust final fields of every class (except java.lang.System) but beware that this is an experimental flag, and violating this assumption is undefined behaviour.