The JVM’s tiered compilation is a sophisticated system that dynamically optimizes Java code by compiling it at different levels of aggressiveness based on how frequently it’s executed.
Let’s see this in action. Imagine a simple Java method:
public class Adder {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
Adder adder = new Adder();
long start = System.nanoTime();
for (int i = 0; i < 1_000_000_000; i++) {
adder.add(i, i + 1);
}
long end = System.nanoTime();
System.out.println("Time taken: " + (end - start) / 1_000_000_000.0 + " seconds");
}
}
When you run this, the JVM doesn’t immediately compile add to highly optimized machine code. Instead, it starts with a quick, less optimized compilation. As the add method is called repeatedly in the main loop, the JVM monitors its execution. After a certain threshold, it recompiles the method at a higher optimization level. If the method continues to be a "hot spot" (frequently executed), it might even go through further, more aggressive optimization stages.
The core problem tiered compilation solves is the trade-off between startup time and peak performance. A naive approach would be to compile all Java code to highly optimized machine code upfront. This would result in very slow application startup because the JVM would spend a lot of time optimizing code that might not even be used. Conversely, if the JVM only ever used a very basic, unoptimized compilation, applications would never reach their full potential performance.
Tiered compilation breaks this down into stages:
- Level 0: Interpreted Mode: The JVM starts by interpreting the bytecode. This is very fast to start but slow to execute.
- Level 1: C1 (Client Compiler) - Level 1 Optimization: If a method is called a few times, it’s compiled by the C1 compiler. This provides a quick compilation with moderate optimizations. It’s a good balance for methods that are called frequently but not extremely so.
- Level 2: C1 Compiler - Level 2 Optimization: Further calls trigger more aggressive C1 optimizations.
- Level 3: C1 Compiler - Level 3 Optimization: Even more C1 optimizations.
- Level 4: C2 (Server Compiler) - Level 4 Optimization: If a method is identified as a "hot spot" (called thousands or millions of times), it’s compiled by the C2 compiler. C2 performs extensive, time-consuming optimizations that yield the highest possible performance but take longer to compile.
The JVM uses "profiling" to decide when to promote a method from one tier to the next. Counters track method invocation counts, back-edge counts (how many times a loop has been entered), and other heuristics. When these counters exceed predefined thresholds, the JVM’s compiler threads kick in.
You can control these tiers with JVM flags. For example, to disable tiered compilation and force the JVM to use only the C2 compiler (effectively setting it to Level 4 optimization immediately, impacting startup), you’d use:
java -XX:TieredStopAtLevel=1 YourMainClass
This command tells the JVM to stop compilation at level 1 (C1’s initial compilation). If you wanted to disable C1 entirely and only use C2, you’d use:
java -XX:-TieredCompilation YourMainClass
This forces the JVM to use only the C2 compiler for all methods, leading to slower startup but potentially better long-term performance for applications with very consistent, high-demand hot spots. The default behavior is usually a good balance.
The most surprising thing about JVM tiered compilation is that the C1 compiler, the "client" compiler, actually has multiple optimization levels itself. It’s not just one step between interpretation and C2. The JVM can trigger more aggressive C1 optimizations based on profiling data before deciding if a method is "hot enough" for the full C2 treatment. This means a method might go through several passes of C1 optimization, each progressively faster, before ever reaching the C2 compiler.
The next concept you’ll encounter is how the JVM handles deoptimization – what happens when the assumptions made during aggressive C2 compilation are invalidated by later runtime behavior.