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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.