I am using AdoptJDk 11.0.7 Java on Windows and have enabled the -XX:+PrintCompilation flag so I can see what methods are being compiled rather just interpreted
I'm invoking some functionality in my application (which process audio files and create an html report on the files). I start the application once (whihch has a limited GUI) and then run the same task over the same set of files a number of times. The second time it's invoked it runs significantly quicker than the first, the third is slightly faster than the second, and then there is not much difference between subsequent runs. But I notice on each run it is still compiling a number of methods, and a lot of methods are becoming non-reentrant.
It is tiered compilation, so I understand that the same method can be recompiled to a higher level but the number of methods being compiled doesn't seem to change much.
I don't understand why so many methods become non-reentrant (and then zombie), I haven't yet done a detailed analysis but it seems the same methods are being compiled over and over again, why would that be ?
I have added the -XX:-BackgroundCompilation
option to force methods to be compiled in order and for the code to wait for the compiled versions rather than using the interpreted version whilst it compiles. This seems to reduce the number of reentrant methods so maybe that is because it reduces the chances of multiple threads trying to access a method that is being (re)compiled ?
But still many methods seem to get recompiled
e.g here I can see it gets compiled to level 3, then it gets compiled to level 4 so level 3 compile is made non-entrant and the zombied. But then level 4 gets non re-entrant, and it goers back to compiling at level 4 and so on.
The JIT compiler helps improve the performance of Java programs by compiling bytecodes into native machine code at run time. The JIT compiler is enabled by default. When a method has been compiled, the JVM calls the compiled code of that method directly instead of interpreting it.
A Just-In-Time (JIT) compiler is a feature of the run-time interpreter, that instead of interpreting bytecode every time a method is invoked, will compile the bytecode into the machine code instructions of the running machine, and then invoke this object code instead.
The JIT compiler aids in improving the performance of Java programs by compiling bytecode into native machine code at run time. The JIT compiler is enabled throughout, while it gets activated when a method is invoked. For a compiled method, the JVM directly calls the compiled code, instead of interpreting it.
The Just-In-Time (JIT) compiler is a component of the Java™ Runtime Environment that improves the performance of Java applications at run time. Java programs consists of classes, which contain platform-neutral bytecodes that can be interpreted by a JVM on many different computer architectures.
The short answer is that JIT deoptimization causes compiled code to be disabled ("made not entrant"), freed ("made zombie"), and recompiled if called again (a sufficient number of times).
The JVM method cache maintains four states:
enum {
in_use = 0, // executable nmethod
not_entrant = 1, // marked for deoptimization but activations
// may still exist, will be transformed to zombie
// when all activations are gone
zombie = 2, // no activations exist, nmethod is ready for purge
unloaded = 3 // there should be no activations, should not be
// called, will be transformed to zombie immediately
};
A method can be in_use
, it might have been disabled by deoptimization (not_entrant
) but can still be called, or it can be marked as a zombie
if it's non_entrant
and not in use anymore. Lastly, the method can be marked for unloading.
In case of tiered compilation, the initial compilation result produced by the client compiler (C1) might be replaced with a compilation result from server compiler (C2) depending on usage statistics.
The compilation level in the -XX:+PrintCompilation
output ranges from 0
to 4
. 0
represents interpretation, 1
to 3
represents different optimization levels of the client compiler, 4
represents the server compiler. In your output, you can see java.lang.String.equals()
transitioning from 3
to 4
. When that happens, the original method is marked as not_entrant
. It can still be called but it will transition to zombie
as soon as it is not referenced anymore.
The JVM sweeper (hotspot/share/runtime/sweeper.cpp
), a background task, is responsible for managing the method lifecycle and marking not_reentrant
methods as zombie
s. The sweeping interval depends on a number of factors, one being the available capacity of the method cache. A low capacity will increase the number of background sweeps. You can monitor the sweeping activity using -XX:+PrintMethodFlushing
(JVM debug builds only). The sweep frequency can be increased by minimizing the cache size and maximizing its aggressiveness threshold:
-XX:StartAggressiveSweepingAt=100 (JVM debug builds only)
-XX:InitialCodeCacheSize=4096 (JVM debug builds only)
-XX:ReservedCodeCacheSize=3m (JVM debug builds noly)
To illustrate the lifecycle, -XX:MinPassesBeforeFlush=0
(JVM debug builds only) can be set to force an immediate transition.
The code below will trigger the following output:
while (true) {
String x = new String();
}
517 11 b 3 java.lang.String::<init> (12 bytes)
520 11 3 java.lang.String::<init> (12 bytes) made not entrant
520 12 b 4 java.lang.String::<init> (12 bytes)
525 12 4 java.lang.String::<init> (12 bytes) made not entrant
533 11 3 java.lang.String::<init> (12 bytes) made zombie
533 12 4 java.lang.String::<init> (12 bytes) made zombie
533 15 b 4 java.lang.String::<init> (12 bytes)
543 15 4 java.lang.String::<init> (12 bytes) made not entrant
543 13 4 java.lang.String::<init> (12 bytes) made zombie
The constructor of java.lang.String
gets compiled with C1, then C2. The result of C1 gets marked as not_entrant
and zombie
. Later, the same is true for the C2 result and a new compilation takes place thereafter.
Reaching the zombie
state for all previous results triggers a new compilation even though the method was compiled successfully before. So, this can happen over and over again. The zombie
state might be delayed (as in your case) depending on the age of the compiled code (controlled via -XX:MinPassesBeforeFlush
), the size and available capacity of the method cache, and the usage of not_entrant
methods, to name the main factors.
Now, we know that this continual recompilation can easily happen, as it does in your example (in_use
-> not_entrant
-> zombie
-> in_use
). But what can trigger not_entrant
besides transitioning from C1 to C2, method age constraints and method cache size contraints and how can the reasoning be visualized?
With -XX:+TraceDeoptimization
(JVM debug builds only), you can get to the reason why a given method is being marked as not_entrant
. In case of the example above, the output is (shortened/reformatted for the sake of readability):
Uncommon trap occurred in java.lang.String::<init>
reason=tenured
action=make_not_entrant
Here, the reason is the age constraint imposed by -XX:MinPassesBeforeFlush=0
:
Reason_tenured, // age of the code has reached the limit
The JVM knows about the following other main reasons for deoptimization:
Reason_null_check, // saw unexpected null or zero divisor (@bci)
Reason_null_assert, // saw unexpected non-null or non-zero (@bci)
Reason_range_check, // saw unexpected array index (@bci)
Reason_class_check, // saw unexpected object class (@bci)
Reason_array_check, // saw unexpected array class (aastore @bci)
Reason_intrinsic, // saw unexpected operand to intrinsic (@bci)
Reason_bimorphic, // saw unexpected object class in bimorphic
Reason_profile_predicate, // compiler generated predicate moved from
// frequent branch in a loop failed
Reason_unloaded, // unloaded class or constant pool entry
Reason_uninitialized, // bad class state (uninitialized)
Reason_unreached, // code is not reached, compiler
Reason_unhandled, // arbitrary compiler limitation
Reason_constraint, // arbitrary runtime constraint violated
Reason_div0_check, // a null_check due to division by zero
Reason_age, // nmethod too old; tier threshold reached
Reason_predicate, // compiler generated predicate failed
Reason_loop_limit_check, // compiler generated loop limits check
// failed
Reason_speculate_class_check, // saw unexpected object class from type
// speculation
Reason_speculate_null_check, // saw unexpected null from type speculation
Reason_speculate_null_assert, // saw unexpected null from type speculation
Reason_rtm_state_change, // rtm state change detected
Reason_unstable_if, // a branch predicted always false was taken
Reason_unstable_fused_if, // fused two ifs that had each one untaken
// branch. One is now taken.
With that information, we can move on to the more interesting example that directly relates to java.lang.String.equals()
- your scenario:
String a = "a";
Object b = "b";
int i = 0;
while (true) {
if (++i == 100000000) {
System.out.println("Calling a.equals(b) with b = null");
b = null;
}
a.equals(b);
}
The code starts off by comparing two String
instances. After 100 million comparisons, it sets b
to null
and continues. This is what happens at that point (shortened/reformatted for the sake of readability):
Calling a.equals(b) with b = null
Uncommon trap occurred in java.lang.String::equals
reason=null_check
action=make_not_entrant
703 10 4 java.lang.String::equals (81 bytes) made not entrant
DEOPT PACKING thread 0x00007f7aac00d800 Compiled frame
nmethod 703 10 4 java.lang.String::equals (81 bytes)
Virtual frames (innermost first):
java.lang.String.equals(String.java:968) - instanceof @ bci 8
DEOPT UNPACKING thread 0x00007f7aac00d800
{method} {0x00007f7a9b0d7290} 'equals' '(Ljava/lang/Object;)Z'
in 'java/lang/String' - instanceof @ bci 8 sp = 0x00007f7ab2ac3700
712 14 4 java.lang.String::equals (81 bytes)
Based on statistics, the compiler determined that the null check in instanceof
used by java.lang.String.equals()
(if (anObject instanceof String) {
) can be eliminated because b
was never null. After 100 million operations, that invariant was violated and the trap was triggered, leading to recompilation with the null check.
We can turn things around to illustrate yet another deoptimization reason by starting of with null
and assigning b
after 100 million iterations:
String a = "a";
Object b = null;
int i = 0;
while (true) {
if (++i == 100000000) {
System.out.println("Calling a.equals(b) with b = 'b'");
b = "b";
}
a.equals(b);
}
Calling a.equals(b) with b = 'b'
Uncommon trap occurred in java.lang.String::equals
reason=unstable_if
action=reinterpret
695 10 4 java.lang.String::equals (81 bytes) made not entrant
DEOPT PACKING thread 0x00007f885c00d800
nmethod 695 10 4 java.lang.String::equals (81 bytes)
Virtual frames (innermost first):
java.lang.String.equals(String.java:968) - ifeq @ bci 11
DEOPT UNPACKING thread 0x00007f885c00d800
{method} {0x00007f884c804290} 'equals' '(Ljava/lang/Object;)Z'
in 'java/lang/String' - ifeq @ bci 11 sp = 0x00007f88643da700
705 14 2 java.lang.String::equals (81 bytes)
735 17 4 java.lang.String::equals (81 bytes)
744 14 2 java.lang.String::equals (81 bytes) made not entrant
In this instance, the compiler determined that the branch corresponding to the instanceof
condition (if (anObject instanceof String) {
) is never taken because anObject
is always null. The whole code block including the condition can be eliminated. After 100 million operations, that invariant was violated and the trap was triggered, leading to recompilation/interpretation without branch elimination.
Optimizations performed by the compiler are based on statistics collected during code execution. The assumptions of the optimizer are recorded and checked by means of traps. If any of those invariants are violated, a trap is triggered that will lead to recompilation or interpretation. If the execution pattern changes, recompilations may be triggered as a result even though a previous compilation result exists. If a compilation result gets removed from the method cache for reasons outlined above, the compiler might be triggered again for the affected methods.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With