Blog

The JIT Compiler in Java

Key Takeaways

  • The Java JIT compiler translates selected bytecode into optimized native machine code while your application runs. In normal HotSpot operation, this happens automatically.
  • javac produces class-file bytecode, not CPU machine code. HotSpot combines interpretation, profiling, and runtime compilation to execute it.
  • HotSpot's tiered compilation uses C1 for relatively quick compilation and profiling, and C2 for more extensive optimization. Methods don't have to visit every tier.
  • Optimizations depend on the code and observed workload. Measure startup and steady-state performance separately, and use profiling tools and JMH rather than a homemade timing loop.

The Java JIT compiler is a runtime compiler that turns frequently executed JVM bytecode into optimized native instructions for the host CPU. It improves performance by replacing repeated interpretation with compiled execution, while using runtime observations to guide optimization. You normally don't invoke it yourself.

The important distinction is between compiling your source and optimizing a running application: javac handles the first job; the JVM's execution engine handles the second. HotSpot is the JVM implementation discussed below, not a requirement that every JVM follow the same execution strategy.

Preface: Compiled vs Interpreted Languages

Ahead-of-Time Compilation

In a conventional C or C++ build, compilation and linking produce a native executable before you run the application. This is ahead-of-time compilation, or AOT. The executable targets a particular platform; that fact alone doesn't establish whether its application will start faster or run faster than a Java application.

Interpreted Execution

An interpreter executes a program's instructions without first compiling each application method to native code. But “compiled” and “interpreted” aren't reliable labels for entire languages: CPython compiles Python source into bytecode, and JavaScript engines such as V8 combine interpretation with JIT compilation. For a related execution model, see our introduction to the V8 JavaScript engine.

Where Java JIT Compilation Fits

Java separates a portable program representation from its execution strategy. javac compiles .java source files into .class files containing JVM bytecode. A compatible JVM executes that bytecode; it may interpret instructions, compile them, or combine those approaches. Native code generated for one CPU isn't what makes the class file portable.

Java source (.java)
        |
      javac
        v
Class-file bytecode (.class)
        |
        v
HotSpot execution
  Interpreter --------------------> program execution
       |
       +-- runtime profiling --> JIT compiler
                                      |
                                      v
                              optimized native code
                                      |
                                      +---> program execution

How the Java JIT Compiler Works

“Hot” code means code active enough to be worth compiling. HotSpot considers method invocations and loop activity, not merely whether a bytecode instruction has appeared twice. Its compilation policy also accounts for profiling information and compiler workload; there isn't one universal invocation count that explains every compilation decision.

  1. Execute and observe: interpreted execution and profiling-enabled compiled code collect information about application behavior.
  2. Select compilation work: HotSpot decides which methods or loops deserve compilation.
  3. Install native code: generated code goes into the JVM's code cache, a memory area for native code.
  4. Adapt: compiled versions can be replaced or invalidated as execution requirements change.

A long-running loop can become hot even when its containing method is called only once. On-stack replacement, or OSR, lets execution move into compiled code while that method invocation is already underway. The code cache is therefore not simply a dictionary of bytecode translations: the JVM manages compiled code, its lifetime, and its validity.

HotSpot C1, C2, and Tiered Compilation

C1 and C2 are historically called the client and server compilers. C1 emphasizes relatively quick compilation; C2 performs more extensive optimization. In tiered compilation, C1-generated code can collect profiles that inform later C2 compilation. These names describe compiler roles, not whether your Java application runs in a browser or on a server.

HotSpot levelExecution modeRole
0InterpreterExecute bytecode; profiling can begin here.
1–3C1-compiled codeLevel 1 has no profiling; level 2 counts calls and loop backedges; level 3 collects fuller profiles.
4C2-compiled codeApply more extensive optimization.

A common route is level 0 → 3 → 4, but levels can be skipped and some methods remain at a lower level. This is an adaptive policy, not a mandatory assembly line.

Tiered compilation predates Java 8: it was introduced in Java SE 7 and became the default for the standard HotSpot server VM in JDK 8. It remains enabled by default in the Oracle JDK 27 server VM. Other JVM implementations may use different compilers and policies.

Speculative Optimization and Deoptimization

Runtime observations let the JIT optimize for behavior it actually sees. For example, an interface call might consistently receive objects of one implementation class. The compiler can optimize for that case while retaining a correctness-preserving path for unexpected behavior.

If an assumption stops holding, HotSpot can deoptimize: reconstruct interpreter execution state from optimized code and continue executing correctly. It may subsequently recompile with different assumptions. Deoptimization is part of adaptive execution, not automatically an application error.

Graal JIT vs GraalVM Native Image

Graal is another optimizing compiler. In GraalVM's JVM mode, it compiles hot bytecode at runtime and serves as an alternative to C2. Graal's HotSpot integration has used JVMCI, the JVM Compiler Interface, in distributions that provide that integration. Compiler availability and selection depend on the distribution and release.

GraalVM Native Image is a separate deployment approach: it compiles application code ahead of time into a platform-specific native executable. It isn't another HotSpot compilation tier. Compare startup, memory consumption, and sustained throughput using your workload rather than assuming either approach always wins.

Java JIT Compiler Optimizations

Inlining, redundant-load elimination, copy propagation, and dead-code elimination all reduce unnecessary work. The following complete program illustrates the opportunities; it doesn't prove that a particular compiler applied them. HotSpot supports these kinds of transformations, but their application depends on context.

Save this as JitExamples.java:

public final class JitExamples {
    static final class Box {
        final int value;

        Box(int value) {
            this.value = value;
        }
    }

    static int increment(int value) {
        return value + 1;
    }

    static int inlineCandidate(int value) {
        return increment(value) + increment(value);
    }

    static int redundantLoads(Box box) {
        int first = box.value;
        int second = box.value;
        return first + second;
    }

    static int copyPropagation(int value) {
        int copy = value;
        return copy + 1;
    }

    static int deadCode(int value) {
        int unused = value * value;
        return value + 1;
    }

    static int scalarReplacement(int value) {
        Box box = new Box(value);
        return box.value + 1;
    }

    static int sum(int[] values) {
        int total = 0;
        for (int i = 0; i < values.length; i++) {
            total += values[i];
        }
        return total;
    }

    public static void main(String[] args) {
        System.out.println(inlineCandidate(5));
        System.out.println(redundantLoads(new Box(5)));
        System.out.println(copyPropagation(5));
        System.out.println(deadCode(5));
        System.out.println(scalarReplacement(5));
        System.out.println(sum(new int[] {1, 2, 3}));
    }
}
javac -d out JitExamples.java
java -cp out JitExamples

This is a functional demonstration, not a benchmark or a workload designed to trigger compilation. A single run doesn't establish which methods became hot.

1. Method Inlining

Inlining brings a called method's body into the caller's compilation. In inlineCandidate, the conceptual result is (value + 1) + (value + 1) instead of two separate calls to increment. Besides avoiding call overhead, this can expose opportunities for further simplification. Field access alone isn't method inlining.

2. Redundant-Load Elimination

redundantLoads reads the same field twice. Here, Box.value is final and no writes or calls intervene, so generated code can reuse the first value while preserving the required behavior if box is null. Don't generalize this to arbitrary shared mutable fields: legal transformations must respect the Java memory model, including volatile and synchronization semantics.

3. Copy Propagation

copyPropagation copies value into copy, then uses the copy. Propagating the original value gives the conceptual expression return value + 1;. Source-level local variables don't necessarily require distinct storage locations in optimized machine code.

4. Dead-Code Elimination

In deadCode, the multiplication produces an unused value and has no observable side effect, so it can disappear. An unused result doesn't make every operation removable: exceptions and other observable effects still matter. The optimization must preserve Java's required behavior.

Additional Optimizations: Allocation and Loops

Escape analysis examines where an object's reference can become accessible. In scalarReplacement, the temporary box doesn't leave the method. If the compiler can analyze the relevant code, scalar replacement can represent its fields directly and eliminate the allocation. This doesn't mean HotSpot simply moves every non-escaping object from heap to stack.

Eliminating allocations can reduce the work left for Java garbage collection. Keep that separate from the basic Java heap-versus-stack distinction; an optimized-away object needs no ordinary object allocation.

Loop optimizations can include unrolling, moving invariant work outside a loop, and eliminating repeated array bounds checks when safety can be established. The straightforward traversal in sum gives the compiler a recognizable relationship between the index and array length. Null and bounds checks aren't permission to ignore invalid inputs: Java's observable exception behavior must remain correct.

Warmup, Startup, and Measurement

JIT compilation has costs: collecting profiles, compiling code, and retaining generated code consume resources. Warmup is the period during which execution and optimization are still developing toward the behavior you want to measure. A short-lived command and a long-running service can therefore have different performance tradeoffs.

Measure cold-start latency separately from warmed-up throughput and request latency. For a microbenchmark, use JMH, the Java Microbenchmark Harness, and follow its samples rather than wrapping a method in a stopwatch loop.

  • Include warmup and separate measurement phases; don't assume a fixed warmup duration suits every workload.
  • Return or consume computed results so dead-code elimination doesn't remove the work being measured.
  • Use representative, nonconstant inputs so constant folding doesn't turn the benchmark into a different computation.
  • Use independent JVM forks to reduce interference from profiles collected by other benchmark cases.

The official JMH samples on dead-code elimination, constant folding, and fork isolation explain these failure modes. A plausible-looking number isn't evidence that your benchmark measures what you intended.

How to Inspect JIT Compilation

The following diagnostic syntax is documented for Oracle JDK 27 HotSpot. Check your actual JDK vendor, release, and deployment environment before using it; JVM diagnostic options aren't universal Java APIs.

Compilation and Inlining Output

-XX:+PrintCompilation prints compilation activity. For more detail, -XX:+PrintInlining prints inlining decisions and requires -XX:+UnlockDiagnosticVMOptions. These options observe compiler decisions; they don't establish that an optimization improved application performance.

# Command templates: replace app.jar with your runnable application JAR.
java -version
java -XX:+PrintCompilation -jar app.jar
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -jar app.jar

Java Flight Recorder

For a running application, Java Flight Recorder records events you can use to investigate runtime behavior. Run jcmd on the same machine with the same effective user and group as the target JVM. Use tools from the matching JDK version.

# Command template: replace 12345 with your application's process ID.
PID=12345
jcmd "$PID" help JFR.start
jcmd "$PID" JFR.start name=jit settings=profile duration=60s filename=jit.jfr

# After the recording finishes, run where jit.jfr was written:
jfr summary jit.jfr

This requests a 60-second recording using the profile configuration. That configuration gathers more information, with more overhead, than default; use it for a bounded investigation. Event settings determine what gets recorded, and jfr summary reports event counts rather than proving every optimization occurred.

Common Mistakes

  • Chasing a magic hotness threshold. Compilation is adaptive; one invocation count isn't a portable rule.
  • Hand-inlining everything. Keep code readable and investigate measured bottlenecks before restructuring it around assumed compiler behavior.
  • Confusing an optimization opportunity with a guarantee. A small source example explains a transformation, not the exact machine code your deployment generates.
  • Changing flags before establishing a baseline. Compare the same workload and environment, change one thing at a time, and separate diagnostic runs from performance measurements.

FAQ

Does javac compile Java directly to machine code?

No. javac normally compiles Java source into class files containing JVM bytecode; a JIT compiler can translate selected bytecode into native machine code while the application runs.

When does the JVM JIT-compile a method?

In HotSpot, compilation decisions depend on method and loop activity, profiling information, and compilation policy. There isn't a single invocation threshold that applies to every method, JVM configuration, and release.

Is the JIT compiler automatically included with Java?

Standard HotSpot-based JDK distributions include JIT compilation, and normal execution enables it automatically without a separate compiler download. The specific compilers available depend on the JVM distribution and build.

What exactly does the JIT compiler optimize in Java?

Depending on the compiler and context, optimizations can include method inlining, redundant-load elimination, copy propagation, dead-code elimination, allocation elimination, and loop transformations. All must preserve Java's required observable behavior.

Can I manually intervene in JIT compilation?

You can influence HotSpot through JVM options and method-specific compiler directives, but ordinary application development doesn't require manually scheduling compilation. Your code and workload also influence optimization; investigate with profiles and benchmarks before changing compiler settings.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me