JIT compilation in Java
Interpreter, C1/C2 tiered compilation, hot spots, inlining, deoptimization.
Start fast, get faster
HotSpot starts by interpreting bytecode: no compile delay, but slow. While interpreting, it counts method calls and loop iterations to find the hot code.
Tiered compilation
Hot methods go through tiers: interpreter → C1 → C2. C1 compiles quickly with light optimization and gathers a profile. C2 uses that profile to produce heavily optimized machine code for the hottest methods.
What the JIT does
Profiling guides optimizations: method inlining, devirtualization, escape analysis, loop unrolling. Not on the list: generic type checking. javac checks generics at compile time and erases them before the JIT ever sees the code.
Why is the start slow?
Why are the first few thousand calls of a method slower than later ones?
Think about it, then reveal the answer
JIT warm-up. Early calls run in the interpreter or as C1 code. Only after the method proves hot does C2 optimize it. That's why benchmarks need a warm-up phase.
Speculate, then deoptimize
C2 sees only Circle reaching shape.area(), so it inlines Circle.area() behind a cheap guard. If a Square shows up, the guard fails and the JVM deoptimizes: it throws that compiled code away, continues safely in the interpreter, and may recompile later.
for (Shape s : shapes)
total += s.area(); // profiled: CircleTiming cold code
Timing a method once, right after startup, measures the interpreter, not the code you'll run in production. Measure only after warm-up, ideally with a harness like JMH.
long t = System.nanoTime();
compute(); // cold: interpreted!
long ns = System.nanoTime() - t;Latency right after a deploy
Fresh instances answer their first requests slowly because nothing is compiled yet. Many teams send warm-up traffic before an instance joins the load balancer, so users never hit cold code.
Key takeaways
- Tiered: interpreter, then C1, then C2
- Profiling guides inlining and devirtualization
- Deoptimization undoes compiled code when assumptions break
- Warm-up: code gets faster after it has run for a while
💡 The JIT is a chef who cooks everything slowly at first, then preps the most popular dishes in advance once the orders reveal them.
Run a program with -XX:+PrintCompilation and the JVM prints each method as it gets JIT-compiled, including its compilation tier.
Practice questions
Why are the first few thousand calls of a method slower than later ones?
- The JVM throttles new applications
- The GC runs more often at startup
- The code is still interpreted or C1-compiled until C2 optimizes it (JIT warm-up)
- Class files are re-read from disk on every call
Check your answer
The code is still interpreted or C1-compiled until C2 optimizes it (JIT warm-up). Performance improves as the JIT compiles hot paths. That's why benchmarks need a warm-up phase.
C2 inlined `shape.area()` assuming only Circle ever reaches that call. Later a Square shows up there. What happens?
- Square's area() is ignored and Circle's runs
- The compiled code is deoptimized; execution falls back to the interpreter and may recompile
- The JVM restarts the method from the beginning with C1 disabled
- A ClassCastException is thrown
Check your answer
The compiled code is deoptimized; execution falls back to the interpreter and may recompile. Speculative optimizations have guards. When a guard fails the JVM throws away that compiled code and continues safely in the interpreter.