Tiered Compilation: Why Modern JVMs Start Fast and Run Faster


The Startup vs. Throughput Tradeoff

For decades, Java applications faced a fundamental tension: you could optimize the JVM for fast startup with interpreted bytecode, or for maximum throughput with aggressive ahead-of-time compilation, but not both. Cloud environments with frequent scaling events and container restarts made this tradeoff more painful. Tiered compilation, now the default in modern JVMs, resolves this by treating compilation as a staged process rather than a binary choice.

The core insight is that different parts of your code need different levels of optimization at different times. Code that runs once during initialization doesn’t warrant the same compilation effort as a tight loop processing millions of requests per second. Tiered compilation dynamically adjusts optimization investment based on observed runtime behavior.

How the Tiers Work

Modern JVMs typically use five compilation tiers, though the exact implementation varies between HotSpot, OpenJ9, and GraalVM. The general pattern moves from interpretation through increasingly sophisticated compilation:

Tier 0 is pure interpretation. The JVM reads bytecode and executes it directly without any compilation overhead. This gets your application running immediately but executes slowly.

Tier 1 applies minimal just-in-time compilation with simple optimizations. Methods get compiled to native code quickly, improving performance without deep profiling.

Tiers 2 and 3 add profiling instrumentation. The JVM collects data about which branches are taken, which types are actually used at polymorphic call sites, and where the hot loops are. This profiling data guides later optimization decisions.

Tier 4 is full optimization using the C2 compiler in HotSpot or equivalent. The JVM applies aggressive inlining, escape analysis, loop unrolling, and speculative optimizations based on the profile data collected earlier. This produces the fastest possible native code but takes significant compilation time.

The Profiling Advantage

What makes tiered compilation particularly powerful is that profile-guided optimization in tier 4 produces better code than ahead-of-time compilation ever could. Static compilers must optimize for all possible execution patterns. The JIT compiler optimizes for the specific execution pattern your application actually exhibits in production.

If a virtual method call always receives the same concrete type in practice, the JIT can inline it and eliminate the virtual dispatch entirely, with a small guard to deoptimize if that assumption ever becomes false. If a branch always goes one way, it can be optimized away. If an object never escapes a method, its allocation can be eliminated completely through scalar replacement.

These optimizations require runtime information that no static compiler has access to. The tiered approach provides that information cheaply by instrumenting lower-tier code before committing to expensive optimization.

Trade-offs and Tuning

The default tiered compilation settings work well for most server applications, but edge cases exist. Extremely short-lived processes that terminate before reaching tier 4 might perform better with ahead-of-time compilation via GraalVM Native Image or OpenJ9’s AOT capabilities. Conversely, applications with highly stable hot paths benefit from aggressive tier 4 compilation thresholds.

The compilation threshold determines how many times a method must execute before promotion to the next tier. Lower thresholds reach peak performance faster but spend more CPU cycles on compilation. Higher thresholds reduce compilation overhead but delay peak performance. In containerized environments with CPU throttling, compilation overhead competes directly with application work for limited CPU quota.

Memory pressure also matters. Each compilation tier and the profile data consume native memory outside the Java heap. In memory-constrained containers, this can trigger OOM kills even when heap usage looks healthy. Monitoring compilation queue depth and CodeCache usage becomes essential in these environments.

Why This Matters Now

Tiered compilation has been the HotSpot default since Java 8, but its importance has grown with cloud-native architectures. Serverless functions, autoscaling pods, and blue-green deployments all create situations where JVM instances start frequently and must reach acceptable performance quickly. The ability to serve requests reasonably well in tier 1 while still optimizing toward tier 4 peak performance makes Java viable in environments that previously favored Go or Rust purely for startup time.

Understanding the tier progression also explains seemingly bizarre performance characteristics. A method that suddenly gets slower might have been deoptimized back to a lower tier after an uncommon branch was taken. A service that performs poorly for the first few minutes after deployment hasn’t reached tier 4 yet. These aren’t bugs; they’re the compilation strategy adapting to observed behavior in ways that ultimately produce faster steady-state performance than any static approach could achieve.