Is Java Interpreted or Compiled? Understanding the JVM’s JIT Execution
This article explains why the JVM combines interpretation and Just‑In‑Time compilation, how it detects hot code with invocation and back‑edge counters, the tiered C1/C2 compilation process, key JIT optimizations like method inlining, and how to observe JIT activity using JVM flags.
Why JIT is needed: interpreter is slow, hot code worth compiling
The JVM first runs bytecode with an interpreter, which provides cross‑platform compatibility and fast startup because the bytecode can be executed immediately. However, interpreting each bytecode instruction on every execution is slow, especially for code that runs many times. To avoid repeated translation, the JVM adds a JIT compiler that translates frequently executed (hot) code into native machine code and caches it, achieving peak performance comparable to or surpassing C++ in some scenarios.
Interpreter : handles quick startup and "cold" code that runs only a few times.
JIT compiler : compiles hot code into native code to achieve peak performance.
The JVM only compiles code that is worth the compilation cost; compiling code that runs only a few times would waste CPU and memory.
Hot Spot Detection: two counters and OSR
Hot spot detection decides which code is hot. HotSpot uses two counters:
Invocation Counter : counts how many times a method is called. When the count exceeds a threshold, the method is considered hot.
Back‑Edge Counter : counts how many times a loop’s back edge is taken. A loop that iterates many times becomes hot even if the surrounding method is called only once.
For methods that are called once but contain a massive loop, the back‑edge counter triggers compilation. The JVM then performs On‑Stack Replacement (OSR): while the method is still running in interpreter mode, the JVM replaces the executing frame with a compiled version so the remaining loop iterations run at native speed.
Method invocation counters decay over time, counting frequency rather than absolute calls, to distinguish sustained hot methods from occasional spikes.
Tiered Compilation: C1 and C2 roles
HotSpot provides two JIT compilers:
C1 (Client Compiler) : fast compilation with modest optimizations, suitable for early‑stage code and startup‑sensitive scenarios.
C2 (Server Compiler) : slower compilation but aggressive optimizations, used for long‑running, performance‑critical code.
Tiered compilation (enabled by default since JDK 8 with -XX:+TieredCompilation) combines both compilers in five levels:
Level 0: Interpreter (collects profiling data)
Level 1: C1, no profiling
Level 2: C1, limited profiling
Level 3: C1, full profiling
Level 4: C2, aggressive optimizationA typical hot method progresses from Level 0 → Level 3 → Level 4, gaining speed quickly with C1 and later receiving the best optimizations from C2.
JIT optimization magic: method inlining and other tricks
Method inlining is the most important JIT optimization. The JIT copies the body of a called method directly into the caller, eliminating call overhead and exposing a larger code region for further optimizations.
public int calcTotal(Order order) {
return getPrice(order) * getCount(order); // two method calls
}
private int getPrice(Order o) { return o.price; }
private int getCount(Order o) { return o.count; }After inlining, the method becomes:
public int calcTotal(Order order) {
return order.price * order.count; // calls removed
}Inlining also enables other optimizations such as escape analysis, scalar replacement, and lock elimination. The JIT prefers to inline small hot methods; large methods are limited by flags like -XX:MaxInlineSize.
Other common optimizations (mentioned for awareness) include common sub‑expression elimination, loop unrolling, dead‑code elimination, and virtual call de‑virtualization.
Deoptimization provides a safety net: if an aggressive assumption (e.g., a virtual call always sees a single implementation) later proves false, the JIT rolls back the optimized code to interpreter mode and may recompile later.
Seeing JIT in action: PrintCompilation and deoptimization
Running a program with -XX:+PrintCompilation prints a log for each JIT compilation:
86 1 3 java.lang.String::hashCode (55 bytes)
102 2 4 com.example.OrderService::calcDiscount (23 bytes)
118 3 % 4 com.example.OrderService::batchProcess @ 12 (98 bytes)Columns explain timestamp, compilation task number, a '%' indicating an OSR compilation, the compilation level (3 = C1 with full profiling, 4 = C2), and the compiled method name with bytecode size. The log shows the transition from interpretation to JIT compilation and highlights OSR events. Deoptimization entries appear as "made not entrant". Tools like JITWatch can parse -XX:+LogCompilation output for visual analysis.
Since JDK 10 an experimental Java‑written JIT called Graal was introduced via -XX:+UseJVMCICompiler , but it was removed in JDK 17; to use Graal now you need the separate GraalVM distribution.
Conclusion
JIT exists because interpretation is portable and fast to start, but slow for repeated execution; compiling hot code yields peak performance.
Hot spot detection uses invocation and back‑edge counters, with OSR handling loops that become hot mid‑execution.
Tiered compilation combines C1 (fast, modest) and C2 (slow, aggressive) across five levels, default‑enabled since JDK 8.
Method inlining is the cornerstone optimization, enabling further tricks like escape analysis and de‑virtualization; deoptimization ensures safety when assumptions fail.
JIT activity can be observed with -XX:+PrintCompilation and visualized with JITWatch; Graal JIT was experimental in JDK 10‑16 and removed in JDK 17.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
