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.
An instance
finalfield 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 finalfield, 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 thestatic finalfields but you will rarely meet those. The logic can be found here.As a result, a
static finalprimitive 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 instancefinalvariable, 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
@Stablethat 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:Stringis deeply immutable, and operations on constant strings can benefit from optimal constant propagation.MethodHandleorVarHandlecan be optimised to a simple method call. With some trickery that I do not fully comprehend, you can even invoke dynamic calls using these (think of a jump table, which can be implemented usingMethodHandles::tableSwitch, or aMutableCallSite, that can arbitrarily change its call target).Integerand friends) can be freely scalar replaced, greatly reduces allocation pressure.In conclusion, a
static finalfield is generally a constant, and a field of a constant is generally not constant itself unless it belongs to a class that is arecord, a hidden class, or a class that is treated specially by the compiler.* This means that changing a
static finalfield 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 ofjava.lang.System, which are specified to be mutable.** Actually the specification do not allow modification of instance
finalfields, but there are a lot of programs out there relying on this ability, most notable are the serialisation libraries. You can use-XX:+TrustFinalNonStaticFieldsto force the compiler to trustfinalfields of every class (exceptjava.lang.System) but beware that this is an experimental flag, and violating this assumption is undefined behaviour.