JIT compilation and HotSpot are with no doubt some very complex topics. This blog is very short article touching on the topic in question; it was originally intended to be included in 97 Things Every Java Programmer Should Know. Contributions for the book had a limit on its length, thus a very short introduction into the HotSpot JIT. In the end, the book authors choose another piece of mine to be included but I thought it this might be interesting for the one or other person out there.
Compared to other compilers, javac
avoids a lot of optimizations when compiling java source code to bytecode. While “Ahead-Of-Time” (AOT) compilation
can do more heavyweight analysis of the source code, a dynamic compiler can take into account runtime statistics like the most used paths (hotspots)
and advanced chipset features (e.g. which CPU instruction sets are available).
Enter the “Just-In-Time” (JIT) compiler. That means over time, the behaviour what and how to compile bytecode to native code changes. Initially, most
bytecode is actually just interpreted (tier 0) which is rather slow. Once a code path is “hot” enough, C1 compiler kicks in (most of us know this by
the -client
flag). It is not as aggressive and allows for a faster initial startup. The C2 compiler (-server
) uses more comprehensive analysis and is
meant for long running processes. Since Java 7, the JVM uses a compilation mode called tiered compilation which seamlessly switches between the modes
based on application behavior.
Initially, the compilers insert profiling probes into the bytecode to determine which code paths are the hottest (e.g. by invocation count), invariants
(which types are actually used) and branch prediction. Once enough analytics are collected, the compilers actually start to compile bytecode to native
code once they are “hot enough” (-XX:CompileThreshold
), replacing the existing byte code step by step (mixed mode execution).
Starting with the hot path, one of the first things the compiler tries to achieve is constant folding. Using partial evaluation and escape analysis,
the compiler will try to determine if certain constructs can be reduced to constants (e.g. the expression 3 * 5
can be replaced with 15
). Another
rather simple optimization is to avoid method calls by inlining methods into their call sites (if they are small enough).
Virtual method calls present a more complex problem. Generally, the best case is a monomorphic call, a method call that can be translated to a direct
jump in assembly. Compare that to polymorphic calls, like an instance method whose type is not known in advance. The type invariants collected previously
by the probes can help tremendously to identify which types are most often encountered within a code path.
The compiler optimizes aggressively using heuristics as well. In case a guess was actually wrong (e.g. the seemingly unused branch was called at some
point), the compiler will deoptimize the code again and may revisit this path later using more profiling data.
Depending on the architecture the JVM is running on, the bytecode may not even be used at all. The HotSpot JVM uses a concept called “intrinsics”
which is a list of well known methods that will be replaced with specific assembler instructions known to be fast. Good examples are the methods in
java.lang.Math
, System#arraycopy
or Object#getClass
(see @HotSpotIntrinsicCandidate
).
Multi-threaded applications may as well benefit from the optimizations the JIT can do with synchronization locks. Depending on the locks used, the
compiler may merge synchronized
blocks together (Lock Coarsening) or even remove them completely if escape analysis determines that nobody else
can lock on those objects (Lock Elision).
You can enable a lot of debug information about how the compiler decides what to do with your code using the feature flags like
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
. If you want to dive deeper into the world of the Hotspot JIT Compiler, have a look at JITWatch.
To have a real deep-dive into such topics, I can highly recommend the posts by Aleksey Shipilëv.