⚙️ JVM Internals & Memory · Advanced

JIT compilation in Java

Interpreter, C1/C2 tiered compilation, hot spots, inlining, deoptimization.

🧩 The mysteryYou benchmark a method: the first 1,000 calls are slow, the next million are lightning fast. Same code, same input. What changed?

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.

🤔 Think first

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: Circle
⚠️ The trap

Timing 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;
💼 In the real world

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

  1. Tiered: interpreter, then C1, then C2
  2. Profiling guides inlining and devirtualization
  3. Deoptimization undoes compiled code when assumptions break
  4. 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.

🤯 Did you know?

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?

  1. The JVM throttles new applications
  2. The GC runs more often at startup
  3. The code is still interpreted or C1-compiled until C2 optimizes it (JIT warm-up)
  4. 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?

  1. Square's area() is ignored and Circle's runs
  2. The compiled code is deoptimized; execution falls back to the interpreter and may recompile
  3. The JVM restarts the method from the beginning with C1 disabled
  4. 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.

The JIT eats bytecode. Next: let's read some ourselves with javap.