Definition
Just-In-Time compilation is how the JVM speeds up Java at runtime. The JVM first interprets bytecode so the program starts quickly, while counting how often each method and loop runs. When a method becomes "hot," the JIT compiler translates its bytecode into optimized native machine code stored in the Code Cache, so future calls run at near-native speed. Tiered compilation combines two compilers: C1 compiles quickly with light optimization to get fast code early, and C2 recompiles the hottest methods with aggressive optimizations for peak performance.
Understanding the Concepts
What is JIT compilation?
When the JVM loads a class, its methods start as bytecode executed by the interpreter. Interpretation means the JVM reads and executes one bytecode instruction at a time. This is slower than native code but needs no compilation, so the application starts immediately. While interpreting, the JVM keeps invocation counters for each method and back-edge counters for loops. These counters are how it measures "hotness."
How does the JIT compiler work?
When a method's counter crosses a threshold, the JIT compiler kicks in on a background thread and compiles that method's bytecode into native machine code, which it stores in the Code Cache. Future calls to the method jump straight into the compiled native code instead of being interpreted, which is typically many times faster.
package com.bank.service;
import java.math.BigDecimal;
public class InterestCalculator {
// This method becomes "hot" during the nightly batch
// After thousands of calls, the JIT compiles it to native code
public BigDecimal calculate(BigDecimal balance, BigDecimal rate) {
return balance.multiply(rate).divide(
new BigDecimal("1200"), 2, java.math.RoundingMode.HALF_UP
);
}
}
What optimizations does the JIT perform?
Because compilation happens on background threads, the application keeps running. The JIT performs powerful optimizations:
- Method inlining: pasting a small called method's body into the caller to remove call overhead.
- Loop unrolling: expanding loop iterations to reduce branch overhead.
- Escape analysis: allocating objects on the stack instead of the heap when they do not escape the method.
- Dead-code elimination: removing code that has no observable effect.
- Constant folding: computing constant expressions at compile time.
What is tiered compilation?
Tiered compilation, enabled by default since Java 8, uses two compilers at different levels.
| Tier | Compiler | Speed | Optimization | Profiling |
|---|---|---|---|---|
| 0 | Interpreter | Immediate | None | Basic counting |
| 1-3 | C1 (client) | Fast compile | Modest | Inserts profiling |
| 4 | C2 (server) | Slow compile | Aggressive | Uses C1 profiles |
A method typically climbs the tiers: interpreted, then C1-compiled with profiling, then, if it stays hot, C2-compiled. This gives both fast startup (from C1) and high peak throughput (from C2).
What is the warm-up period?
Right after startup, code runs interpreted or only C1-compiled, so it is slower until C2 has had time to optimize the hot paths. This is why benchmarks must warm up the JVM before measuring, and why latency-sensitive services sometimes pre-warm critical paths.
# Observe JIT compilation in real-time
java -XX:+PrintCompilation -jar bank-api.jar
# Output shows which methods are compiled at which tier:
# 1234 42 4 com.bank.service.InterestCalculator::calculate (25 bytes)
# ^seq ^comp ^tier=4 (C2) ^bytecode size
What is deoptimization?
The JIT can also deoptimize: if an optimization assumed something (like a method never being overridden or a branch never being taken) that later becomes false, it falls back to interpretation and recompiles. This is safe because the JVM can always interpret bytecode.
What should a senior engineer know?
- Code Cache sizing:
-XX:ReservedCodeCacheSizecontrols how much memory is available for compiled native code. If the cache fills, the JIT stops compiling and performance degrades. - GraalVM provides an alternative JIT compiler (Graal) and an ahead-of-time (AOT) compiler that produces native binaries with instant startup but no JIT optimization.
- In banking at National Bank of India, the first transfers after a deployment are noticeably slower than later ones. This is the JIT warm-up effect. Rajesh pre-warms the critical transfer path right after startup so that by the time real customer traffic arrives, the hot paths are already C2-compiled.
Key Takeaway
Just-In-Time compilation is how the JVM gives Java both fast startup and high peak performance. When a program starts, its methods are bytecode run by the interpreter, which executes one instruction at a time. That is correct and immediate but slower than native code. While it interprets, the JVM counts how often each method and loop runs to measure which ones are hot. Once a method crosses a hotness threshold, the JIT compiler translates its bytecode into optimized native machine code on a background thread and stores it in the Code Cache, so every future call jumps straight into that native code and runs many times faster. Along the way the JIT applies powerful optimizations like inlining small methods, unrolling loops, and escape analysis.
Tiered compilation, which is on by default, is the clever part. It uses two compilers. The C1 compiler compiles quickly with light optimization and adds profiling, so hot methods get decent native code early. The C2 compiler is slower but applies aggressive, profile-guided optimizations to the very hottest methods for peak throughput. A method typically climbs the tiers, from interpreted, to C1-compiled, to C2-compiled, giving you both quick startup and the fastest steady-state code. The JIT can even deoptimize and fall back to interpretation if an assumption it made turns out to be wrong, then recompile.
In my banking work at National Bank of India, this matters in two concrete ways. Our interest calculation runs millions of times per batch, so the JIT compiles it to native code early and the rest of the batch runs dramatically faster than pure interpretation would allow. And we had to explain to the team why the very first transfers after a deployment were noticeably slower than later ones. That is the JIT warm-up effect: the hot paths have not been C2-optimized yet. Knowing this, for latency-sensitive services we deliberately pre-warm the critical methods right after startup so that by the time real customer traffic arrives, the hot paths are already compiled and running at full speed. I confirm which methods get compiled and at which tier using PrintCompilation, so I know our most important banking methods are reaching the C2 level.
Remember one sentence: What is JIT (Just-In-Time) compilation: JVM interprets bytecode first, then compiles hot methods to native code for faster execution: and what is tiered compilation (C1 compiler for quick compilation + C2 compiler for aggressive optimization) is fundamental knowledge for Java interviews and production banking work.
---