Does the JVM Eliminate Allocations of Temporary Objects?
Yes, including arrays, but only under certain conditions. Most notably, references must not leave the current stack frame (unless callees can be inlined), and called object methods must be eligible for inlining.
Join the DZone community and get the full member experience.Join For Free
Should you be concerned about temporary Java objects or can the JVM eliminate them for you, maybe by replacing them with implicit static instances?
Consider the following example:
The JVM does not implicitly share this object in terms of a static object, which would violate the Java Language Specification. Even if the object itself had no fields, the behavior could change if it relies on other objects and their state, which might not be thread-safe. Even if the object was thread-safe or the code is single-threaded, implicit reuse could break the assumptions of the original code as the implementation does not always start from the initial state.
As for eliminating the object allocation itself, it could be done and is done under specific circumstances. By means of escape analysis, the JIT can determine if references to a newly created object can theoretically leave or "escape" the current frame in terms of the current set of local variables in which case references might be stored in fields of other heap objects or static fields. If the analysis finds that a given object does not, the heap allocation could be replaced with a stack allocation as the lifetime of the object is restricted to the current frame.
OpenJDK performs escape analysis. The compiler represents the outcome as follows:
With that information, allocations can be optimized. However, the JVM does not replace heap allocations with stack allocations. Instead, it executes an optimization known as "scalar replacement" which means that object field accesses are replaced with corresponding local variables if object references do not escape the current frame and all called object methods can be inlined. The JVM thereby completely eliminates the actual object instance. Depending on the implementation of
ObjectMapper, this optimization could apply.
To illustrate the effect of scalar replacement, we'll focus on this example:
TestProcessor instance never leaves the current frame, it does not "escape" it. Running the example without scalar replacement (
-XX:-EliminateAllocations) triggers the garbage collector:
With default options, the GC is not triggered at all and the total runtime is considerably less pronounced:
Storing the instance reference in an object field, array element, or a static field disables scalar replacement, even if the object field belongs to an object that does not escape the frame. All four cases in the modified example do:
Inlining is a requirement for scalar replacement. If the object's methods or the code that accesses the object to be scalar-replaced cannot be inlined, the object is not suitable for scalar replacement. The reason is simple: the code must access object fields and those fields are represented by local variables in the current frame, so the code must run in the current frame to gain local variable access.
Adding an object access that can be inlined (
Object.equals()) does not prevent scalar replacement:
In that scenario, overriding
hashCode() enables scalar replacement again because the new method can be inlined:
The inlining decision is also affected by the method size. In the original example, the method
TestProcessor.process() was small enough. The example calls the method numerous times, so the relevant limit is controlled by
-XX:FreqInlineSize. The default in my tests based on Java 14 is
Increasing the method size by means of a
switch block allows us to exceed the limit:
Method size before:
offset 23 + sizeof(areturn) = 24:
Method size after:
offset 331 + sizeof(areturn) = 332:
By exceeding the default limit of
325, scalar replacement is disabled again.
In Java, arrays are objects. Consequently, arrays can be subject to scalar replacement. However, the constraints are significant:
The array size must not exceed
-XX:EliminateAllocationArraySizeLimit=<value>. The default is
64. Also, the size must be constant. The stack frame size is usually constant, so is the concept of local variables. This limitation makes sense.
Similarly, the index used for element access must be constant. Iterating an array with a loop is incompatible with scalar replacement.
The JVM source reveals more constraints concerning the object definition itself:
In terms of real-world relevance, the most important aspect is that overriding
Object.finalize() disables scalar replacement. Finalization requires the object to be accessible when the hook is called - this implies
Less relevant is a field limit configurable via
-XX:EliminateAllocationFieldsLimit=<value>. The default is
512, so it's unlikely to cause problems.
Scalar Replacement vs. Stack Allocation
Scalar replacement is a valuable optimization. It certainly isn't a catch-all solution for eliminating the allocation overhead of temporary objects and it never intended to be. As with other optimizations, there are constraints that need to be satisfied.
Stack allocation has the potential to eliminate the inlining dependency because object references could be passed directly but comes with their own set of implications and could turn out to be less efficient. Stack allocation requires representing the object in memory, scalar replacement on the other hand can potentially represent object fields in registers.
If you know that the object can safely be shared and reused based on its documentation, the best approach is to explicitly reuse the instance rather than relying on potential optimizations that can vary from one JVM implementation to another and, more importantly, might be disabled due to caller/callee changes in the future.
Published at DZone with permission of George R. See the original article here.
Opinions expressed by DZone contributors are their own.