Tiered Compilation in Java
When a Java program starts, the JVM doesn't compile everything to machine code at once. It uses tiered compilation: code first runs in the interpreter, which starts instantly but is slow; methods that are called often are compiled quickly by the C1 compiler; and the hottest methods are recompiled with heavy optimizations by the C2 compiler. This gives fast startup and excellent peak speed. The basics of the JIT compiler were introduced in "JIT Compiler"; in this lesson, you will learn the tiers, see them in a real compilation log, and measure how much each one matters.
The Tiers
| Level | Runs as | Profiling | Purpose |
|---|---|---|---|
| 0 | Interpreter | Yes | Start running immediately |
| 1 | C1 code, no profiling | No | Simple methods that won't benefit from C2 |
| 2 | C1 code, limited profiling | Some | Used when C2 is busy |
| 3 | C1 code, full profiling | Yes | Collect data (types, branches) for C2 |
| 4 | C2 code, fully optimized | No | Peak performance for hot code |
A typical hot method goes 0 → 3 → 4: interpreted, then C1 with profiling, then C2 using the profile to optimize aggressively.
Seeing It Happen
This program calls a small method two million times:
public class Hot {
static int square(int x) {
return x * x;
}
public static void main(String[] args) {
long sum = 0;
for (int i = 0; i < 2_000_000; i++) {
sum += square(i % 1000);
}
System.out.println(sum);
}
}With -XX:+PrintCompilation, the JVM logs each compilation. The lines for this program on JDK 25:
java -XX:+PrintCompilation Hot10 8 3 Hot::square (4 bytes)
10 9 4 Hot::square (4 bytes)
10 8 3 Hot::square (4 bytes) made not entrant: not used
11 10 % 3 Hot::main @ 4 (36 bytes)
11 11 3 Hot::main (36 bytes)
11 12 % 4 Hot::main @ 4 (36 bytes)Explanation: The columns are time in milliseconds, a compilation ID, a % for on-stack replacement (OSR), the tier, and the method. Within 10 milliseconds, square was compiled at tier 3 (C1) and then tier 4 (C2); the tier-3 version was then "made not entrant", meaning thrown away because the better version exists. main contains the long loop, so it was compiled while still running using OSR (%), first by C1, then by C2.
Example: How Much Each Tier Matters
The same program with a 200 million-iteration loop, run three ways and timed on the test machine:
java -Xint Hot2
java -XX:TieredStopAtLevel=1 Hot2
java Hot2| Mode | Time |
|---|---|
-Xint (interpreter only) | about 2,570 ms |
-XX:TieredStopAtLevel=1 (C1 only) | about 116 ms |
| Default (all tiers, C2 for hot code) | about 102 ms |
Explanation: Without any JIT compiler, the program is about 25 times slower. C1 alone recovers most of the speed for this simple loop; C2's extra optimizations add more. In bigger programs, the gap between C1 and C2 is usually larger.
Common Mistakes
Timing code the first time it runs. It's still interpreted or partly compiled.
Using -Xint or -XX:TieredStopAtLevel=1 in production for speed. They lower peak performance (C1-only can help short-lived tools start faster).
Reading "made not entrant" as an error. It's normal: an older compiled version was replaced.
Interview Corner
What is tiered compilation?
Tiered compilation is HotSpot's default strategy of combining the interpreter with two JIT compilers. Methods start interpreted (tier 0) while their invocation and loop counts are tracked; warm methods are compiled quickly by C1 with profiling (tier 3), and hot methods are recompiled by the optimizing C2 compiler (tier 4) using the collected profile. On-stack replacement lets long-running loops switch to compiled code mid-execution. This balances fast startup with high peak performance; wrong speculative assumptions trigger deoptimization back to the interpreter.
Practice
- Easy: Run any program with
-XX:+PrintCompilationand find one method compiled at tier 4. - Harder: Time a loop-heavy program with
-Xint,-XX:TieredStopAtLevel=1and the default, and explain the differences.
Key Takeaways
Was this tutorial helpful?