Java Tutorial
🔍

Java JIT Compiler

Java JIT Compiler

The JIT, or Just-In-Time, compiler is the part of the JVM's execution engine that watches bytecode as it runs and compiles the parts that run often into native machine code, so the JVM stops re-interpreting the same instructions on every single call. It is the specific mechanism that lets Java, a language that ships as portable bytecode rather than a native binary, still compete with natively compiled languages once a program has been running long enough to warm up.

What Is the JIT Compiler?

The JIT compiler is a component of HotSpot's execution engine, sitting alongside the bytecode interpreter and the garbage collector. Where the interpreter reads and executes bytecode instructions one at a time, translating them on the fly every time, the JIT compiler produces actual native machine code for a given piece of bytecode once, so future calls run that native code directly instead of being interpreted again.

Why the JIT Compiler Exists

A pure interpreter re-decodes the same bytecode instructions every time a method runs, no matter how many times it has already run identically before — for a method called a handful of times this cost is trivial, but for a method called millions of times, that repeated translation work adds up. A fully ahead-of-time compiled language avoids this by compiling everything to native code before the program ever starts, but that trade comes at a real cost: it ties the resulting binary to one specific operating system and CPU architecture, and it has to make every optimization decision statically, with no information about how the code actually behaves once real data starts flowing through it.

The JIT compiler is Java's answer to both problems at once. The program starts as portable bytecode and runs interpreted immediately, with no large upfront compilation delay and no loss of portability. Once the JVM notices a method or loop is genuinely hot — running often enough that compiling it is clearly worth the cost — it compiles that specific piece of code to native machine code, using real profiling data gathered while it was still interpreted to make optimization decisions a purely static compiler could never make with the same confidence.

How JIT Compilation Works Internally

Every method starts out interpreted. The JVM maintains an invocation counter for each method, and a related back-edge counter for loops, both incrementing every time that method runs or that loop iterates. Once a counter crosses an internal threshold, the JVM flags that code as hot and hands it to the JIT compiler running on its own background compiler threads, so compilation never pauses the application thread that triggered it.

One sentence before the diagram: HotSpot uses two compilers with different tradeoffs, and tiered compilation is what lets a method benefit from both instead of picking just one.

Bytecode (method first called)
        |
        v
   Interpreter  ------ runs immediately, gathers profiling data
        |
        | invocation counter crosses threshold
        v
  C1 (client compiler)  ------ compiles quickly, adds instrumentation,
        |                      moderate optimization
        | method stays hot, profiling data accumulates
        v
  C2 (server compiler)  ------ recompiles using the profiling data,
                               aggressive optimization, slower to compile
                               but produces the fastest native code

C1 favors compilation speed over optimization depth, historically suited to workloads where startup responsiveness mattered most. C2 favors optimization depth over compilation speed, historically suited to long-running server workloads where peak throughput matters more than how quickly compilation itself finishes. Tiered compilation, HotSpot's long-standing default strategy, runs both in sequence — C1 compiles a method quickly once it warms up and keeps collecting profiling data, and C2 later recompiles that same method with full optimization once it proves it is hot enough to be worth the extra compilation cost.

A compiled method is not necessarily permanent. If an assumption the JIT made during compilation turns out to be wrong — for example, it compiled a call site assuming only one implementation of an interface existed, and a newly loaded class introduces a second one — the JVM can de-optimize that method back to interpreted execution and, if it is still hot, recompile it later with updated assumptions.

Code shaped like a tight, frequently-called loop is exactly what invocation and back-edge counters are watching for, though a short program that exits after a handful of calls, like the one below, never runs long enough to meaningfully benefit from that in practice — the point here is the shape of the code, not a timing claim about this specific run.

1// File: HotLoopExample.java 2 3public class HotLoopExample { 4 5 static long calculateTotal(int[] amounts) { 6 long total = 0; 7 for (int amount : amounts) { 8 total += amount; 9 } 10 return total; 11 } 12 13 public static void main(String[] args) { 14 int[] dailyOrderAmounts = new int[1_000_000]; 15 for (int i = 0; i < dailyOrderAmounts.length; i++) { 16 dailyOrderAmounts[i] = 100; 17 } 18 19 long total = 0; 20 for (int day = 0; day < 5; day++) { 21 total = calculateTotal(dailyOrderAmounts); 22 } 23 24 System.out.println("Total for one day: " + total); 25 } 26}
Output:
Total for one day: 100000000

calculateTotal() is called five times here, each time summing a million-element array — a real production service calling a method shaped like this millions of times over its lifetime is precisely the case tiered compilation exists for, even though this short-lived demo never runs long enough to observe that benefit directly.

Real-World Example

A checkout service applies a discount to every order price as it processes a day's orders, calling the same small calculation method repeatedly — exactly the pattern that benefits most from JIT compilation once the service has been running under real traffic.

1// File: DiscountCalculator.java 2 3public class DiscountCalculator { 4 5 public double applyDiscount(double price, double discountPercent) { 6 return price - (price * discountPercent / 100); 7 } 8}
1// File: CheckoutBatchProcessor.java 2 3public class CheckoutBatchProcessor { 4 5 private final DiscountCalculator discountCalculator = new DiscountCalculator(); 6 7 public double processDailyOrders(double[] orderPrices, double discountPercent) { 8 double totalRevenue = 0; 9 for (double price : orderPrices) { 10 totalRevenue += discountCalculator.applyDiscount(price, discountPercent); 11 } 12 return totalRevenue; 13 } 14}
1// File: CheckoutBatchDemo.java 2 3public class CheckoutBatchDemo { 4 public static void main(String[] args) { 5 double[] orderPrices = new double[1000]; 6 for (int i = 0; i < orderPrices.length; i++) { 7 orderPrices[i] = 500.0; 8 } 9 10 CheckoutBatchProcessor processor = new CheckoutBatchProcessor(); 11 double totalRevenue = processor.processDailyOrders(orderPrices, 10.0); 12 13 System.out.println("Total revenue after discount: " + totalRevenue); 14 } 15}
Output:
Total revenue after discount: 450000.0

applyDiscount() is a small, simple method with a stable, predictable shape — exactly the kind of call the JIT compiler both inlines and compiles aggressively once it runs often enough. A mistake that appears often in fresher pull requests is assuming a tight, frequently-called calculation like this needs to be manually rewritten in a lower-level style for performance, when the JVM's own JIT compiler is specifically designed to recognize this exact pattern and compile it to native code without any help from the developer. Premature manual optimization here usually just makes the code harder to read for a gain the JIT would likely have delivered on its own.

Best Practices

Write small, well-factored methods instead of one large method meant to avoid call overhead — the JIT is very effective at inlining small hot methods, and a large method is generally harder to compile and optimize aggressively than several small ones.

Avoid premature micro-optimization aimed at helping the JIT — let it work on naturally written, readable code, and reach for JVM-level tuning only after profiling has actually identified a measured, real bottleneck.

Remember that short-lived scripts and quick command-line tools see far less benefit from JIT compilation than a long-running service does, since meaningful compilation requires enough repeated execution to actually warm up.

Keep hot call sites reasonably stable rather than routing them through many different implementations of the same interface — highly polymorphic call sites are harder for the JIT to optimize as confidently as ones that consistently resolve to the same one or two implementations.

Common Mistakes

Judging Java's performance from a quick benchmark of a program that starts, runs briefly, and exits is a common and genuinely misleading mistake — a program that short-lived may finish before its hot code ever gets compiled at all, meaning the benchmark measured interpreted, cold-start performance rather than the steady-state performance a real long-running service would actually see.

Reaching for JVM flags like -Xint, which forces pure interpretation and disables the JIT compiler entirely, outside of narrow diagnostic or debugging scenarios is another mistake worth avoiding — it discards the entire throughput benefit tiered compilation provides and should never be part of a normal production configuration.

Interview Questions

Q1. What is the JIT compiler, and where does it fit in the JVM's execution engine?

It is the component of HotSpot's execution engine that compiles frequently executed bytecode into native machine code at runtime, working alongside the bytecode interpreter rather than replacing it entirely. The nuance interviewers listen for is understanding that both the interpreter and the JIT compiler are active in the same running JVM, not that one replaces the other.

Q2. Why doesn't the JVM just interpret bytecode the whole time, or compile everything to native code upfront?

Pure interpretation re-translates the same instructions every time code runs, which wastes work on frequently executed code. Compiling everything upfront loses portability and cannot use real runtime profiling data to make better optimization decisions. The JIT compiler gets both fast startup and strong steady-state performance by starting interpreted and selectively compiling only what proves to be hot.

Q3. What triggers the JVM to JIT-compile a particular method?

An invocation counter tracks how often a method is called, and a related back-edge counter tracks loop iterations — once either counter crosses an internal threshold, the JVM flags that code as hot and schedules it for compilation on a background compiler thread. Interviewers are listening for whether you know this is counter-driven and threshold-based, not that every method gets compiled immediately.

Q4. What is the difference between C1 and C2 compilation in HotSpot?

C1, the client compiler, compiles quickly with lighter optimization and adds profiling instrumentation. C2, the server compiler, compiles more slowly but applies much more aggressive optimization, using the profiling data C1 gathered to make better decisions. The production insight here is that tiered compilation runs both in sequence rather than forcing a choice between them.

Q5. What is tiered compilation, and why does it combine C1 and C2 instead of picking one?

Tiered compilation runs a method through the interpreter first, then C1 once it warms up, then C2 if it proves hot enough to justify the heavier optimization cost. Combining them gets fast responsiveness early and peak throughput later, rather than trading one for the other by picking a single compilation strategy for the whole application.

Q6. What is de-optimization, and when does it happen?

De-optimization is the JVM reverting a compiled method back to interpreted execution because an assumption made during compilation turned out to be invalid — commonly when a newly loaded class breaks an assumption about how many implementations of an interface exist at a given call site. It can later be recompiled with updated assumptions if it is still hot.

Q7. Does the JIT compiler change what a program's output is, or only how fast it runs?

Only how fast it runs. Compiled native code and interpreted bytecode are required to produce identical, observable program behavior — the JIT compiler is a performance optimization layered underneath the language's semantics, never a change to what a correct program actually computes.

FAQs

Does every method get JIT compiled eventually?

No. Only methods that run often enough to cross the JVM's invocation or back-edge thresholds get compiled — a method called only a handful of times over a program's entire lifetime typically stays interpreted the whole time, since compiling it would cost more than it saves.

Can I see which methods the JIT has compiled?

Yes, -XX:+PrintCompilation is a real JVM flag that prints compilation events to standard output as they happen during a run, useful for diagnosing warm-up behavior in a specific application.

Does the JIT compiler run on a separate thread from my application code?

Yes, compilation happens on dedicated background compiler threads, so compiling a method does not pause the application thread that triggered the compilation — the application keeps running on the interpreted or previously compiled version until the newly compiled version is ready to swap in.

Is AOT (ahead-of-time) compilation available in Java at all?

Yes, through GraalVM Native Image, which compiles a Java application to a native executable ahead of time with no JIT or interpreter involved at runtime. The JDK's own experimental jaotc tool, added in Java 9, was later removed from the JDK in Java 17 as part of JEP 410.

Does JIT compilation happen once per method, or can a method be recompiled?

A method can be recompiled more than once — first by C1 with instrumentation, later by C2 with full optimization, and potentially recompiled again after a de-optimization if its behavior changes and it remains hot.

Why does a Java program sometimes feel slower in its first few seconds than later?

This is commonly called JVM warm-up — early execution runs on the interpreter or lightly optimized C1 code before the JIT compiler has had a chance to compile the application's hot paths with full optimization, so performance tends to improve as more of the running code gets compiled.

Is the JIT compiler part of every JVM, or specific to HotSpot?

The C1/C2 tiered design described in this article is specific to HotSpot, the JVM implementation shipped with OpenJDK and most production distributions. The general concept of just-in-time compilation is common across most modern production-grade JVMs, but the specific compiler architecture and thresholds differ by implementation.

Summary

The JIT compiler is what lets Java start up quickly as portable, interpreted bytecode and still reach native-level performance for the code that actually matters, by watching invocation and loop counters, compiling hot code through C1 first and C2 later, and using real runtime profiling data that a purely static, ahead-of-time compiler never has access to.

The habit worth carrying forward from this article's checkout example is trusting the JIT to optimize naturally written, small, well-factored methods rather than hand-rewriting hot code out of a guess about what might be slow — and remembering that any performance claim about Java needs a long-running, warmed-up workload behind it, not a quick script that exits before compilation ever kicks in.

What to Read Next