JIT Compilation: Tiered Compilation, C1, C2, GraalVM
- Last updated
- Prerequisites:
- Java bytecode and class loading basics
- Understanding of interpretation vs compilation
- java
- jit
- c1
- c2
- graalvm
- compilation
- hotspot
Read at your depth
The practical view
The JVM starts by interpreting bytecode, then the JIT compiler compiles hot methods to native machine code at runtime. HotSpot uses tiered compilation by default: C1 (client compiler) compiles quickly with moderate optimization, and C2 (server compiler) does deep optimizations for very hot methods. You can observe with -XX:+PrintCompilation and control with -XX:TieredStopAtLevel. GraalVM provides an alternative JIT (and an AOT compiler for native images). JIT means Java code gets faster the more it runs — benchmarks must warm up.
The same idea in other frameworks
N/A — JVM runtime concern.N/A — JVM concern.N/A — JVM concern.N/A — JVM concern.Legacy vs modern
Interpreted baseline vs tiered JIT-compiled execution
Running cold code interprets bytecode; hot methods get compiled to native machine code with profiling-driven optimization.
// ❌ OLD: assuming all Java code runs as compiled native code from the startfor (int i = 0; i < 10_000; i++) sum(1_000); // ✅ NEW: warm up so C1/C2 compileInterview gotchas
Context
This tests the interpretation→profiling→compilation model — interviewers want the thresholds and tier progression, not just 'the JIT compiles hot code'.
The mechanical answer
The JVM starts by interpreting bytecode (level 0) so execution begins immediately. The interpreter (and later C1 with profiling) collects per-method data: invocation counts, backedge counts (loops), and type profiles of call sites, stored in method data objects. When a method's counters cross the compilation thresholds (e.g. -XX:CompileThreshold, plus tiered thresholds), it is queued to the compiler broker: first C1 with profiling (tier 2/3) for quick native code, then, if still hot, C2 (tier 4) for deep optimization using the collected profile. This lazy, profile-guided progression is why cold code is interpreted (fast startup), warm code gets quick compilation, and hot code gets maximal optimization — and why microbenchmarks must include warmup to reflect steady state.
Trap
The naive answer is 'the JIT compiles everything at startup'. Compilation is demand-driven and profile-guided, happening after thresholds. Another trap: thinking compiled code is permanent — deoptimization (uncommon traps) reverts to interpretation when a guarded optimization (e.g. an inlined call's assumed type) is violated. Candidates who mention thresholds, MDO profiling, and deoptimization show real understanding.
Context
This tests knowledge of the compiler stack and the alternatives — interviewers want to know what each compiler optimizes for and where GraalVM fits.
The mechanical answer
C1 (client compiler) is a quick compiler: it produces decent native code fast using a simple linear-scan register allocator, prioritizing fast compilation time (good for startup and short-lived processes). C2 (server compiler) is a heavyweight optimizing compiler: it builds a Sea-of-Nodes (Ideal Graph) IR and runs an extensive optimization pipeline — inlining, escape analysis, scalar replacement, loop optimizations, and vectorization — with a global register allocator. GraalVM's JIT (used with -XX:+UseGraal in some builds, and as the default in GraalVM's JDK) is an alternative to C2 written in Java, using a graph-based IR with partial escape analysis and a different inlining/optimization strategy; it also powers GraalVM Native Image via the same compiler in AOT mode. The choice affects peak throughput and warmup characteristics.
Trap
The naive answer is 'C1 is faster than C2'. C1 compiles FASTER; C2 produces FASTER code. Another trap: assuming GraalVM is only for native images — it also provides a JIT compiler that can replace C2. Candidates who distinguish compile-time vs runtime performance and name the IR differences show real depth.