How the JVM Decides What to Compile: Inside the JIT Tier System and Profiling Pipeline
A conceptual walkthrough of C1, C2, tiered compilation, and why the JVM’s optimizer knows more about your code than you do.
Java is often described as a compiled language and an interpreted one, depending on who’s explaining it, and both answers are a little bit right. The JVM starts every method as interpreted bytecode, then quietly promotes the ones worth optimizing into native machine code — sometimes more than once, using two entirely different compilers with different priorities. This staged promotion is called tiered compilation, and understanding it explains a lot of otherwise mysterious JVM behavior, from slow warm-ups to sudden performance cliffs after a config change.
Why the JVM Doesn’t Just Compile Everything Upfront
Compiling every method to optimized native code the moment the application starts would sound like the fastest option, but it isn’t. Aggressive optimization takes real time and CPU, and most methods in a typical application run only a handful of times — compiling them at all would be wasted effort. The JVM’s actual strategy is closer to triage: run everything as interpreted bytecode first, watch closely to see which methods actually get hot, and spend the expensive optimization budget only where it will pay off.
This is the same idea behind why HotSpot got its name in the first place: the runtime is built around finding the small number of “hot” methods that dominate execution time, rather than treating every line of code as equally important.
Two Compilers, Two Different Jobs
C1: The Client Compiler
C1 exists to get a method out of the slow interpreter as quickly as possible. It applies a modest set of optimizations and compiles fast, prioritizing low compilation overhead over maximum runtime performance. C1 is also responsible for attaching lightweight profiling instrumentation to the compiled code, so the JVM keeps learning about the method’s behavior even after it stops being interpreted.
C2: The Server Compiler
C2 is the more aggressive, more patient optimizer. It performs deeper inlining, more thorough loop optimizations, and speculative techniques based on the profiling data gathered earlier, at the cost of a much longer and more CPU-intensive compilation process. C2 is reserved for methods that have proven, through actual measured execution, that they’re worth the investment.
The Five Levels of Tiered Compilation
Since Java 7, HotSpot runs both compilers together in a single pipeline rather than forcing a choice between them. A method moves through as many as five distinct execution levels, and the JVM decides the next step dynamically based on invocation counts, loop back-edge counts, and how busy each compiler’s queue currently is.
| Level | Compiler | Profiling | Purpose |
|---|---|---|---|
| 0 | Interpreter | None | Default starting point for every method |
| 1 | C1 | None | Fast exit for trivial methods that don’t need deeper analysis |
| 2 | C1 | Light (invocation and back-edge counters) | Used under heavy C2 queue pressure |
| 3 | C1 | Full profiling | Default path most hot methods take before C2 |
| 4 | C2 | None (already fully optimized) | Peak long-term performance for proven hot methods |
The path from level 0 to level 4 is usually 0 → 3 → 4, but the JVM can route through level 2 instead of 3 when the C2 compilation queue is under pressure, trading some profiling depth for reduced compiler contention. This adaptive routing is part of why two runs of the same application can warm up slightly differently depending on system load.

Profiling: The Data C2 Depends On
C2’s aggressive optimizations aren’t guesses — they’re built on real profiling data collected while a method ran at levels 2 or 3. Branch frequencies, actual types seen at a call site, and which paths through the code are common versus rare all feed into decisions like inlining a virtual call or eliminating a null check the profiler never saw fail. This is what makes C2’s output potentially more efficient than what an ahead-of-time compiler could produce with no runtime information at all: it optimizes for the code paths your application actually takes, not every path that’s theoretically possible.
That same speculation is also why deoptimization exists. If an assumption baked into optimized code turns out to be wrong later — a previously monomorphic call site suddenly sees a new type, for instance — the JVM discards the optimized version and falls back to the interpreter for that method until it can safely recompile.

What This Means for How You Write and Benchmark Code
The tiered model explains two things developers frequently misdiagnose. First, benchmarking a method for a few hundred iterations often measures interpreted or C1 performance, not the C2-optimized steady state the code will reach in production — this is precisely why microbenchmarking tools like JMH insist on explicit warm-up iterations before measuring. Second, short-lived processes such as CLI tools or serverless functions may never accumulate enough invocations to reach level 4 at all, meaning C2’s deepest optimizations simply never get a chance to apply.
Keep these in mind when reasoning about JIT behavior:
- A method needs to run many times before it’s considered worth C2’s more expensive optimization pass.
- Warm-up time in benchmarks and short-lived processes reflects real tiered compilation behavior, not a measurement artifact to ignore.
- Deoptimization is a normal, expected safety net for speculative optimizations, not a sign of something broken.
- Compiler queue pressure can change which tier a method routes through, so behavior can vary slightly run to run under load.
What We Learned
Tiered compilation exists because no single strategy — pure interpretation, pure fast compilation, or pure aggressive optimization — serves an entire application’s lifetime well on its own. C1 gets hot methods out of the interpreter quickly while continuing to gather profiling data, and C2 spends its heavier optimization budget only on methods that have already proven, through real execution counts, that the investment will pay off. The five-level pipeline that connects them is the JVM continuously re-deciding, method by method, how much it’s worth knowing about your code before committing to a final optimized version.

