The Java Virtual Machine (JVM) doesn’t just run your Java code; it actively rewrites it on the fly to make it faster, and the secret sauce is "tiered compilation."

Let’s watch it in action. Imagine a simple loop that adds numbers:

public class Summation {
    public static void main(String[] args) {
        long sum = 0;
        for (int i = 0; i < 1000000000; i++) {
            sum += i;
        }
        System.out.println("Sum: " + sum);
    }
}

When you first run this, the JVM’s JIT (Just-In-Time) compiler kicks in. It doesn’t immediately produce the absolute fastest code. Instead, it starts with "quick and dirty" compilation.

The Tiers of Speed

Tiered compilation is like a multi-stage rocket. Each stage burns fuel differently and serves a specific purpose.

  • Tier 0: Interpreted Mode. This is the default. The JVM reads your bytecode instruction by instruction. It’s slow but gets your application running immediately. No compilation overhead at startup.

  • Tier 1: C1 (Client Compiler). After a method has been called a few times (a "hotness threshold"), the C1 compiler steps in. It performs basic optimizations and compiles the method to native machine code. This is much faster than interpretation, but the optimizations are relatively light. It prioritizes fast compilation time.

  • Tier 2: C2 (Server Compiler). If a method gets really hot (called many, many times), the C2 compiler takes over. This is the heavy artillery. C2 performs aggressive, time-consuming optimizations like inlining, loop unrolling, and dead code elimination. The resulting code is highly optimized for execution speed, but the compilation itself takes longer.

  • Tier 3: Profile Guided Optimization (Optional). In some JVMs, there’s an intermediate tier or a way for C2 to use profiling data gathered by C1 to make even smarter optimization decisions.

The JVM dynamically decides which tier to use based on how often methods are executed. It’s a continuous process: a method starts interpreted, gets compiled by C1 when it’s "warm," and if it becomes "hot," C2 takes over for maximum performance.

The Mental Model: Balancing Startup and Steady State

The core problem tiered compilation solves is the trade-off between fast application startup and high long-term performance.

  • Traditional JIT (All C2): If the JVM only had a powerful, optimizing compiler like C2, your application would take a very long time to start because compiling everything upfront is slow. It would be fast eventually, but the initial wait would be painful.

  • Interpretation Only: If the JVM only interpreted, your application would start instantly but would never achieve high performance, especially for CPU-bound tasks.

Tiered compilation provides the best of both worlds. It starts fast (interpretation) and gets progressively faster as the application runs and the JIT compilers optimize the hot code paths.

The Levers You Control

While tiered compilation is mostly automatic, you can influence it:

  • JVM Flags: You can explicitly tell the JVM to use specific compilation modes.

    • -client: Forces the JVM to use only the C1 compiler and interpretation. Good for applications with very fast startup requirements and less long-term compute.
    • -server: Forces the JVM to use C1 and C2 compilation. This is the default for most modern JVMs and is best for long-running server applications.
    • -XX:TieredStopAtLevel=X: This flag allows you to control the highest compilation tier used. X=1 means only C1, X=4 (the default) means C1 and C2. Setting this to a lower number can reduce compilation overhead at the cost of peak performance.
  • Application Design: While not a JVM flag, writing code that has clear "hot" methods and "cold" methods helps the tiered compiler do its job efficiently. Avoid excessively large methods that might be harder for the compiler to analyze and optimize.

The Surprise: C1 is Not Just a "Pre-C2"

Most people think of C1 as just a stepping stone to C2. But C1 has its own distinct compilation strategy. It prioritizes quick compilation and generates code that is significantly better than interpreted code, but it doesn’t spend the time C2 does on deep, complex optimizations. C1’s goal is to get code running fast enough quickly, allowing profiling data to be gathered that C2 can then use. C1’s optimizations are also more conservative, meaning it’s less likely to make a wrong assumption that could lead to incorrect code, which is crucial for early-stage compilation.

The next thing you’ll bump into is understanding how the JVM manages memory after your code is running at peak speed.

Want structured learning?

Take the full Java course →