Java path · From source to running code · Topic 3 of 12

JIT compilation: interpreter, C1, C2

Not started

The JVM Specification says what bytecode means, not how to run it fast. HotSpot's answer is adaptive compilation: start every method in an interpreter, watch which methods and loops actually get used, and compile those to machine code while the program keeps running. Everything in this topic is HotSpot behaviour, not part of Java itself; other JVMs make different choices.

Why it matters: Why a Java service is slow for its first minute, why a naive benchmark lies, and why code that looks expensive often isn't.

  1. 1Interpret and count

    HotSpot starts every method in the interpreter and counts how often it is called and how often its loops go round.

  2. 2Two compilers in tiers

    Warm methods are compiled quickly by C1, which also records a profile; hot ones are recompiled by C2 using that profile.

  3. 3Speculate, then back out

    C2 optimises for what it has seen so far. If an assumption stops holding, the code is deoptimised and runs in the interpreter again.

  4. 4Warm-up is real

    Measure after the JIT has settled, with a harness such as JMH, never with one timed loop.

1

How it works

The explanation, with examples you can run.

Tiers

With tiered compilation, which is on by default, a method can run at five levels:

  • Tier 0, the interpreter: starts immediately, collects counts, runs slowly.
  • Tiers 1 to 3, the C1 (client) compiler: compiles fast with simpler optimisations. At tier 3 the generated code also records a profile: which branches are taken, which classes really appear at each call site.
  • Tier 4, the C2 (server) compiler: slower to compile, but uses the profile for aggressive optimisation.

A typical hot method goes 0, then 3, then 4. The switch points are counters with thresholds (in JDK 21, a method is queued for tier 3 after a couple of hundred calls and for tier 4 after several thousand), adjusted for how busy the compiler threads are. Compilation runs on background threads, so your code keeps running in the interpreter meanwhile.

Watching it

public class Hot {    static long square(long x) { return x * x; }     public static void main(String[] args) {        long sum = 0;        for (int i = 0; i < 50_000_000; i++) sum += square(i);        System.out.println(sum);    }}

java -XX:+PrintCompilation Hot prints one line per compilation. You will see Hot::square compiled at tier 3 and then 4, the tier 3 version "made not entrant" once tier 4 is ready, and lines marked % for Hot::main. Those are on-stack replacement (OSR) compilations: main is only called once, so HotSpot compiles the loop and swaps it in while it is still running.

What C2 does with the profile

  • Inlining: copying small, hot callees into the caller. This is the optimisation that unlocks the others, and it is why small methods and getters cost nothing in hot code.
  • Speculation: if a call site on an interface has only ever seen one class, C2 can call that class's method directly, guarded by a cheap type check. Branches that never ran can be left out entirely.
  • Escape analysis: if an object never leaves the compiled method, C2 can skip allocating it and keep its fields in registers (scalar replacement).

Speculation needs a way back. When a guard fails, a new subclass is loaded, or a never-taken branch is finally taken, HotSpot deoptimises: it rebuilds the interpreter's frames from the compiled ones, continues in the interpreter, and may compile again later with the new profile.

Warm-up

All of this means a JVM is slowest just after start-up. A service may need thousands of requests before its hot paths reach C2, so load balancers sometimes ramp traffic to new instances slowly. It also means a hand-written benchmark is easy to get wrong: the first iterations measure the interpreter, and a result you never use may be removed as dead code. Use JMH, the OpenJDK benchmark harness, which handles warm-up, forking and dead-code elimination for you.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.In HotSpot's default tiered mode, how does a hot method normally progress?
  2. 2.-XX:+PrintCompilation shows a line ending in "made not entrant". What happened?
  3. 3.You time one loop of 1,000 calls right after start-up and conclude a method is slow. What is wrong with that measurement?

0 of 3 right so far. Get all three to finish this topic.