JVM JIT 101
JIT compilation and HotSpot are with no doubt some very complex topics. This blog is a short article touching on JIT.
Join the DZone community and get the full member experience.Join For Free
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 behavior of 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, the 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 has used 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
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 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.
Published at DZone with permission of Benjamin Muskalla. See the original article here.
Opinions expressed by DZone contributors are their own.