Skip to main content

JIT Compiler in Java: From Bytecode to Machine Code

Introduction to JIT Compilation​

When you compile a Java program with javac, the output is bytecode β€” a platform-neutral intermediate representation stored in .class files. Bytecode is not machine code; it cannot execute directly on a CPU. Instead, the Java Virtual Machine (JVM) must either interpret the bytecode or compile it into native machine code. Just-In-Time (JIT) compilation is the process by which the JVM translates bytecode into native machine code at runtime, while the application is running.

The core insight behind JIT compilation is that the JVM can observe how the application behaves during execution and use that information to generate better-optimized machine code than a static compiler could produce. Because the compiler runs inside the running JVM, it has access to dynamic information that is simply unavailable to a traditional ahead-of-time (AOT) compiler:

  • Which methods are called most frequently
  • What types are actually used at call sites
  • How branches behave in practice
  • Which code paths are hot

This ability to observe and adapt makes JIT compilation a cornerstone of Java's performance characteristics.

Interpretation vs. Compilation​

The JVM can execute bytecode in two fundamentally different ways:

AspectInterpreterJIT Compiler
Execution modeBytecode instruction by instructionCompiled native code
Startup timeFastRequires compilation time
Warm-upMinimalRequired
Long-running performanceUsually lowerUsually higher
Runtime optimizationLimitedExtensive
Memory overheadLowHigher (code cache)
Platform-specific optimizationNoYes

The interpreter is simple, reliable, and starts executing immediately. The JIT compiler takes time to analyze, compile, and optimize, but produces code that runs much faster. Modern JVMs use both: they start with interpretation and gradually compile hot code as the application runs.

Java Source (.java)
↓
javac
↓
Bytecode (.class)
↓
JVM
↓
Interpreter (fast startup)
↓
Runtime Profiling (observing behavior)
↓
JIT Compilation (hot methods)
↓
Optimized Machine Code (peak performance)

This hybrid approach gives Java applications the best of both worlds: fast startup from the interpreter and excellent peak performance from the JIT compiler.

Where JIT Fits in the JVM Architecture​

The JIT compiler is part of the JVM's Execution Engine. To understand its role, we need to see how it fits into the broader JVM architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Java Bytecode (.class) β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Class Loader β”‚
β”‚ (Loading, Linking, Initialization) β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Runtime Data Areas β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Method Area β”‚ Heap β”‚ Stack β”‚ PC Registers β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Execution Engine β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚ β”‚ Interpreter β”‚β”‚
β”‚ β”‚ (Executes bytecode initially) β”‚β”‚
β”‚ β”‚ β”‚ β”‚β”‚
β”‚ β”‚ β–Ό β”‚β”‚
β”‚ β”‚ Profiler β”‚β”‚
β”‚ β”‚ (Collects runtime execution data) β”‚β”‚
β”‚ β”‚ β”‚ β”‚β”‚
β”‚ β”‚ β–Ό β”‚β”‚
β”‚ β”‚ JIT Compiler β”‚β”‚
β”‚ β”‚ (Translates bytecode β†’ optimized native code) β”‚β”‚
β”‚ β”‚ β”‚ β”‚β”‚
β”‚ β”‚ β–Ό β”‚β”‚
β”‚ β”‚ Native Machine Code β”‚β”‚
β”‚ β”‚ (Executes on CPU) β”‚β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ CPU β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key relationships:

  • Class Loader: Provides the bytecode that the execution engine processes.
  • Runtime Data Areas: Provide the memory context (heap, stacks, method area) where compiled code executes and accesses data.
  • Interpreter: The fallback execution mode that handles all code initially and continues to execute code that is not hot enough to compile.
  • Garbage Collector: Interacts with compiled code through safepoints and memory barriers.
  • CPU: Executes the native machine code generated by the JIT compiler.

The JIT compiler does not operate in isolation β€” it works closely with the interpreter (which feeds it profiling data) and the GC (which must cooperate with compiled code).

Interpreter vs JIT Compiler: Detailed Comparison​

The Interpreter: Simple and Immediate​

The interpreter executes bytecode one instruction at a time. For each bytecode, the interpreter fetches the instruction, decodes it, and performs the corresponding operation. This is straightforward but slow because:

  • Each bytecode requires multiple native operations.
  • No optimization is applied across bytecode boundaries.
  • There is overhead from instruction fetch and decode.

However, the interpreter has distinct advantages:

  • Immediate startup: No compilation delay.
  • No warm-up required: The application begins doing useful work instantly.
  • Low memory overhead: No code cache needed for compiled code.
  • Deterministic behavior: No optimization surprises.

The JIT Compiler: Optimized and Adaptive​

The JIT compiler translates entire methods (or code regions) into native machine code. This transformation is expensive but yields code that runs much faster because:

  • Multiple bytecodes are combined into optimized native instructions.
  • The compiler can apply high-level optimizations (inlining, constant folding, dead code elimination).
  • Generated code can leverage CPU-specific instructions and execution modes.
Bytecode sequence:
iconst_1
istore_1
iload_1
iconst_2
iadd
istore_2

Interpreted: Each bytecode is decoded and executed separately.
Compiled: The entire sequence can become a few native instructions,
often with the constants folded at compile time.

The JIT compiler's value increases with application runtime: the longer an application runs, the more it benefits from compilation and optimization.

Why JVMs Use Both​

The "both" approach enables:

  • Fast startup (interpreter starts instantly)
  • Gradual optimization (profiling identifies hot code)
  • Peak performance (compiled code for hot methods)
  • Adaptability (recompilation as behavior changes)
Application Lifetime
β”œβ”€β”€ Startup Phase: Interpreter only
β”œβ”€β”€ Warm-up Phase: Interpreter + profiling + initial compilation
β”œβ”€β”€ Steady State: Mostly compiled code with some interpretation
└── Degradation (rare): Deoptimization + recompilation

HotSpot and Modern JVM Compilation​

HotSpot is the JVM implementation developed by Sun Microsystems (now Oracle) and open-sourced as part of OpenJDK. It is the most widely used JVM implementation and the reference implementation for modern Java.

The name "HotSpot" reflects the core strategy: the JVM identifies hot spots β€” frequently executed methods or code regions β€” and focuses compilation effort on those areas. Instead of compiling every method (which would be slow and memory-intensive), HotSpot profiles execution and compiles only what matters.

Hot Code Detection​

The JVM identifies hot code through:

  • Method invocation counts: Methods called many times are candidates.
  • Loop back-edge counts: Loops that run many iterations trigger compilation even if the containing method is not called frequently.
  • Branch profiles: Information about which branches are taken.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Cold Code β”‚
β”‚ (Rarely executed) β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Interpreted β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Profiling Counters Increase β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Threshold Reached? ────No────► Continue β”‚
β”‚ β”‚ Interpreting β”‚
β”‚ Yes β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Hot Code Detectedβ”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ JIT Compilation Queued β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Optimized Native Code β”‚
β”‚ (Hot Code) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Compilation Thresholds​

The JVM does not compile every method. Compilation is triggered when code exceeds a compilation threshold, which is typically based on the number of invocations or loop iterations. These thresholds are tunable (via -XX:CompileThreshold, though the exact flags vary by version), but the defaults are set to balance startup time, compilation overhead, and peak performance.

Adaptive Optimization​

HotSpot's JIT compiler is adaptive:

  • It uses profiling data to guide optimization decisions.
  • It can recompile code with more aggressive optimizations as more profile data becomes available.
  • It can deoptimize and revert to interpretation if assumptions become invalid.

JIT Compilation Pipeline​

The JIT compilation process follows a pipeline with several stages, from bytecode input to executing optimized machine code:

Bytecode
↓
Interpretation
↓
Profiling (Method + Loop counters, type profiles, branch profiles)
↓
Hot Code Detection (Threshold exceeded)
↓
Compilation Request (Queued on compiler thread)
↓
Optimization (Inlining, escape analysis, dead code elimination, etc.)
↓
Machine Code Generation (Instruction selection, register allocation)
↓
Native Machine Code (Stored in Code Cache)
↓
Execution (CPU runs compiled code)

Stage 1: Interpretation​

When an application starts, all bytecode is executed by the interpreter. This provides immediate execution with no compilation delay. During interpretation, the JVM collects profiling data.

Stage 2: Profiling​

Profiling is the critical enabler of JIT optimization. The interpreter (and some compiled code) records:

  • Invocation counts: How many times each method is called.
  • Back-edge counts: How many times a loop iteration completes.
  • Type profiles: Which concrete types are passed to a method or used in casts.
  • Branch profiles: Which branch is taken, with counts.
  • Exception profiles: Where exceptions are thrown.

Stage 3: Hot Code Detection​

The JVM maintains counters for methods and loops. When a counter exceeds a configurable threshold, the method is considered "hot" and scheduled for compilation.

Stage 4: Compilation​

The JIT compiler takes the bytecode of a hot method and transforms it into optimized native machine code. This is a multi-step process involving:

  • Bytecode parsing and IR generation
  • High-level optimizations (inlining, escape analysis)
  • Low-level optimizations (register allocation, instruction selection)
  • Code generation (emitting machine code for the target CPU)

Stage 5: Execution​

Once compiled, the native machine code is stored in the Code Cache (a region of the JVM's memory) and executed directly on the CPU. Future invocations of the method use the compiled code instead of the interpreter.

JIT Compilation Tiers​

Modern HotSpot JVMs use tiered compilation, which employs multiple compilation levels to balance startup speed, compilation overhead, and peak performance.

The Conceptual Tiers​

  • Tier 0 (Interpreter): Bytecode interpretation with profiling.
  • Tier 1 (C1 with no profiling): Lightweight compilation for methods that benefit from compilation but don't need aggressive optimization.
  • Tier 2 (C1 with limited profiling): C1 compilation with some profiling information.
  • Tier 3 (C1 with full profiling): C1 compilation with comprehensive profiling. This tier can be used as a compilation level for methods that will later be recompiled by a higher-tier compiler.
  • Tier 4 (C2/maximum optimization): Full, aggressive optimization for the hottest methods.

Note: Tier numbers and exact behavior are HotSpot implementation details and may change across Java versions. The key concept is that modern HotSpot can compile code at different levels of optimization, using simpler compilers for fast startup and more sophisticated compilers for peak performance.

Tiered Compilation Flow​

Interpreter (Tier 0)
β”‚
β”‚ Profiling data accumulates
β–Ό
C1 with full profiling (Tier 3)
β”‚
β”‚ More profiling data accumulates
β–Ό
C2 optimized compilation (Tier 4)
β”‚
β–Ό
Peak performance execution

Why Tiered Compilation Matters​

  • Startup: Simple compilation tiers get code compiled quickly.
  • Profiling: Lower tiers collect profile data for higher tiers.
  • Peak performance: Higher tiers use profile data for aggressive optimization.
  • Memory efficiency: Not all code needs C2-level optimization.

Important: Tiered compilation is a HotSpot/OpenJDK feature, not a requirement of the JVM specification. Other JVM implementations may use different compilation strategies.

C1 and C2 Compilers​

Within the HotSpot JVM, two primary compilers have historically served different roles. Understanding them provides insight into the HotSpot architecture, though Java developers should recognize these as implementation details rather than fundamental concepts of the JVM.

C1 (Client Compiler)​

C1 is designed for fast compilation. It performs:

  • Relatively simple bytecode-to-native translation
  • Basic optimizations (constant folding, dead code elimination)
  • Minimal analysis overhead
  • Lower memory footprint

Trade-offs:

  • Faster compilation β†’ better startup
  • Less aggressive optimization β†’ lower peak performance
  • Suitable for client applications, short-running applications, and early compilation tiers

C2 (Server Compiler)​

C2 is designed for peak performance. It performs:

  • Extensive code analysis
  • Aggressive optimizations (inlining, escape analysis, loop unrolling, vectorization)
  • Speculative optimization based on profiling data
  • Longer compilation times

Trade-offs:

  • Slower compilation β†’ delayed startup
  • More aggressive optimization β†’ higher peak performance
  • Suitable for long-running server applications

C1 and C2 in Tiered Compilation​

In a tiered compilation environment, C1 and C2 are used together:

  • Methods are initially compiled by C1 for fast turnaround.
  • Profiling data is collected from C1-compiled code.
  • Hottest methods are recompiled by C2 for peak performance.
  • The JVM may switch between tiers as behavior changes.

Note: C1 and C2 naming comes from historical HotSpot compiler codebases ("Client" and "Server" compilers). Modern Java versions have evolved significantly. Graal JIT, available as an experimental alternative in OpenJDK, represents a different compiler architecture.

Profiling-Guided Optimization​

The JIT compiler's ability to outperform static compilers comes from profiling-guided optimization (PGO) β€” the use of runtime data to inform compilation decisions.

What Profiling Captures​

  • Invocation frequencies: Which methods are called most often.
  • Branch behavior: Which branches are taken and how often.
  • Type information: The actual types of objects at specific program points.
  • Class hierarchy: Which classes and subclasses exist at runtime.
  • Allocation behavior: Which objects are allocated and where.
  • Locking patterns: How locks are contended.

How Profiling Guides Optimization​

Runtime Observation
↓
Profile Data
↓
Optimization Assumption
↓
Specialized Machine Code
↓
Guard (Check assumption at runtime)
↓
If Valid: Run optimized code
If Invalid: Deoptimize

Example: A method call list.get(i) on a List interface. The JIT observes that list is almost always an ArrayList. It optimizes the call as if it is always ArrayList.get(), with a guard to verify the type. If a LinkedList is later used, the guard fails and the JVM deoptimizes.

Why Profiling Matters​

Static compilers (like GCC or GraalVM's native-image) compile ahead of time and cannot make assumptions based on runtime behavior. They must generate code that works for all possible cases, often sacrificing performance for generality.

JIT compilers, by contrast, can specialize code for the actual observed runtime behavior, producing faster code for the common case while retaining correctness for all cases through deoptimization.

Key JIT Optimizations​

The JIT compiler applies a rich set of optimizations. These are not exhaustive β€” modern JITs perform many more β€” but they represent the most important and frequently discussed transformations.

Method Inlining​

Inlining replaces a method call with the body of the called method. This eliminates call overhead and exposes the called method's body to the caller's optimizations.

Conceptual example:

// Without inlining:
int result = add(a, b);

// With inlining:
// The method body is inserted directly
int result = a + b;

Why inlining matters:

  • Eliminates method call overhead (stack frames, argument passing, return handling).
  • Enables further optimizations (constant propagation, dead code elimination, escape analysis) across method boundaries.
  • Can cascade: inlined methods may reveal further inlining opportunities.

Practical considerations:

  • The JIT inlines at compile time based on heuristics (method size, call frequency, etc.).
  • Inlining is speculative: the JIT may inline based on type profiling.
  • Excessively deep inlining can lead to code bloat and increased compilation time.

Dead Code Elimination​

Dead code elimination removes code that cannot be executed or whose results are never used. This reduces code size and eliminates unnecessary work.

Example:

int x = 5;
if (false) {
// This code is removed
System.out.println("Never executed");
}
int y = x * 2; // x is a constant, so y is constant too
// The computation can be folded or eliminated if unused

Variants:

  • Unreachable code removal: Code guarded by false conditions.
  • Unused result elimination: Results of computations that are never used.
  • Speculative dead code: Code that is unreachable based on profiling (e.g., an exception path that never executes).

Constant Folding​

Constant folding evaluates constant expressions at compile time (or at JIT compile time) rather than at runtime.

Conceptual example:

// Before optimization:
int x = 5 * 3 + 2;

// After constant folding:
int x = 17;

Extended version:

for (int i = 0; i < 100; i++) {
// Constant folding can compute 100 * 2 at compile time
process(i * 2);
}

Runtime constants: The JIT can also fold values that are not constant in the source code but are constant at runtime (e.g., final fields, values determined by class initialization).

Loop Optimization​

Loops are critical for performance. The JIT applies several loop optimizations.

Loop Invariant Code Motion​

Code that produces the same result on every loop iteration can be moved outside the loop.

Before:

for (int i = 0; i < n; i++) {
int threshold = computeThreshold(); // Same every time
if (i > threshold) { ... }
}

After:

int threshold = computeThreshold(); // Moved outside
for (int i = 0; i < n; i++) {
if (i > threshold) { ... }
}

Loop Unrolling​

Loop unrolling replicates the loop body to reduce iteration overhead.

Before:

for (int i = 0; i < 100; i++) {
process(i);
}

After (partial unrolling):

for (int i = 0; i < 100; i += 4) {
process(i);
process(i+1);
process(i+2);
process(i+3);
}

Note: Loop unrolling is complex and only applied when beneficial. Excessive unrolling can cause code bloat and instruction cache pressure.

Escape Analysis​

Escape analysis determines whether an object escapes the method or thread where it is allocated.

Object escape states:

  • No escape: Object is only used within the allocating method and never escapes.
  • Local escape: Object escapes the method but not the thread.
  • Global escape: Object can escape the thread.

Based on escape state, the JIT can:

  • Scalar replacement: Replace the object with its fields (primitive values) when it does not escape.
  • Stack allocation: The JIT may allocate the object on the stack, but note that in many modern JIT implementations, this is typically handled through scalar replacement (allocating individual fields as scalar values) rather than directly "allocating" an object on the stack.
  • Lock elimination: Remove unnecessary synchronization for thread-local objects.

Conceptual example:

public int compute() {
Point p = new Point(10, 20); // May not escape
return p.x + p.y; // JIT can replace with: return 10 + 20;
}

After escape analysis, the Point object may be scalar replaced with direct values 10 and 20, eliminating allocation and GC pressure.

Lock Elision and Lock Coarsening​

Lock Elision​

When the JIT determines that a lock is only acquired by one thread, it can remove the lock completely.

Example:

public synchronized void method() { ... }
// If this method is only called from one thread,
// the JIT may eliminate the synchronization.

Lock Coarsening​

Lock coarsening merges multiple lock acquisitions into a larger critical section, reducing lock overhead.

Before:

synchronized (lock) { doA(); }
synchronized (lock) { doB(); }

After:

synchronized (lock) { doA(); doB(); }

Important: These optimizations preserve the Java Memory Model's visibility and ordering guarantees. The JIT cannot remove synchronization in a way that would violate the JMM.

Speculative Optimization​

Speculative optimization is a defining characteristic of JIT compilation: the compiler makes optimistic assumptions based on runtime observations and generates optimized code that relies on those assumptions.

How Speculation Works​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Runtime Observation β”‚
β”‚ (e.g., type profile) β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Optimization Assumption β”‚
β”‚ (e.g., "This call site always uses ArrayList") β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Optimized Machine Code with Guard β”‚
β”‚ (e.g., "if (type != ArrayList) deoptimize") β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ Assumption Valid Assumption Invalid β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β–Ό β–Ό β”‚
β”‚ Executes Optimized Deoptimization β”‚
β”‚ Code ────► Fallback β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Common Speculative Optimizations​

  • Type profiling: Optimizing a virtual/interface call as if it always goes to a specific implementation.
  • Branch prediction: Optimizing code based on the most common branch outcomes.
  • Method inlining: Inlining based on type assumptions at call sites.
  • Value profiling: Optimizing based on common values (e.g., String.length() often returns the same value for a given string).

Example of Type-Based Speculation​

// Source code
List<String> list = getList(); // Could be ArrayList or LinkedList
list.get(0);

// JIT observes: list is always ArrayList
// Generates optimized code:
if (list is ArrayList) {
// Direct ArrayList.get() implementation
// No interface dispatch, no extra indirection
} else {
// Deoptimize to less optimized code
}

Deoptimization​

Deoptimization is the JVM's mechanism for "undoing" optimistic optimizations when the assumptions that guided compilation are violated.

What Deoptimization Is​

When the JIT compiles a method speculatively, it generates guards β€” runtime checks that validate the assumptions made during compilation. If a guard fails, the code cannot continue executing correctly. The JVM must then:

  1. Pause execution of the compiled code.
  2. Restore the state to a point where interpretation (or less-optimized code) can continue.
  3. Transfer control to the interpreter or recompiled code.
  4. Discard or invalidate the compiled code.

Why Deoptimization Exists​

Deoptimization enables aggressive optimization. Without it, the JIT would need to generate conservative code that works for all possible cases. Deoptimization allows the compiler to "bet" on the common case and recover if the bet is wrong.

Deoptimization in Practice​

Scenario: A method process(Animal animal) is called 10,000 times with Dog instances. The JIT optimizes the code as if animal is always a Dog, inlining Dog-specific operations.

Then a Cat is passed:

Compiled Code (optimized for Dog)
β”‚
β–Ό
Guard: is animal instanceof Dog?
β”‚
β”œβ”€β”€ Yes: Continue execution (fast)
β”‚
└── No: Guard fails ──► Deoptimization
β”‚
β–Ό
Interpreter
or recompiled
code

Deoptimization and Performance​

Deoptimization has a cost:

  • The JVM must reconstruct the state (stack frames, local variables) so the interpreter can continue.
  • The compiled code is invalidated (or marked for recompilation).
  • Recompilation may occur with updated profile data.

However, deoptimization is rare in well-behaved applications. If a method's behavior is stable, deoptimization never occurs.

Machine Code Generation​

From Bytecode to Native Code​

Bytecode is platform-independent. Machine code is platform-specific. The JIT compiler's final stage translates the optimized intermediate representation into actual machine code for the target CPU architecture.

Bytecode (.class)
β”‚
β”‚ Platform-independent
β–Ό
JIT Compiler (Optimization + Code Generation)
β”‚
β”‚ Platform-specific
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Native Machine Code β”‚
β”‚ β”‚
β”‚ x86-64 JVM ──► x86-64 instructions β”‚
β”‚ ARM64 JVM ──► ARM64 instructions β”‚
β”‚ Other CPUs ──► Platform-specific instructions β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Machine Code Generation Components​

  • Instruction selection: Choosing which CPU instructions to use for each operation.
  • Register allocation: Assigning values to CPU registers for fast access.
  • Instruction scheduling: Ordering instructions to maximize CPU pipeline utilization.
  • Code layout: Organizing code for optimal cache and branch prediction behavior.

Code Cache​

Compiled native code is stored in the Code Cache, a memory region managed by the JVM. The code cache is separate from the heap and stacks.

Code Cache characteristics:

  • Size-limited: The code cache has a maximum size (-XX:ReservedCodeCacheSize).
  • Non-GC: Compiled code is not garbage collected unless explicitly invalidated.
  • Performance-critical: Fast code cache access is essential for performance.
  • Monitoring: Code cache usage can be monitored via JFR or JMX.

Platform-Specific Optimization​

The JIT can generate code optimized for the specific CPU:

  • Instruction set extensions (AVX, SSE, etc.)
  • Cache sizes (instruction and data cache optimization)
  • CPU pipeline characteristics (instruction scheduling)
  • Core count (parallelization decisions)

JIT Warm-Up​

What Warm-Up Means​

JIT warm-up refers to the period during which the JVM compiles methods and reaches peak performance. During warm-up, the application may run slower than its steady-state performance because:

  • Methods are still being interpreted.
  • Compilation is happening (consuming CPU time).
  • Profiling data is being collected.
  • Code is being optimized and recompiled.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ JIT Warm-Up Lifecycle β”‚
β”‚ β”‚
β”‚ Startup β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Interpretation + Initial Profiling β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Method Invocation Counts Reach Threshold β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Tier 1 (C1) Compilation β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Tier 3 (C1 with profiling) β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Tier 4 (C2/max) Compilation β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ Steady State (Peak Performance) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why Warm-Up Matters​

Warm-up is a key consideration for:

  • Startup performance: Applications that need fast startup (e.g., serverless functions, batch jobs) may not reach peak performance before their execution completes.
  • Load testing: Load tests that start immediately may observe slower performance during the warm-up period.
  • Benchmarking: Microbenchmarks must account for warm-up to avoid misleading results.
  • Capacity planning: Production applications may need to be warmed up before handling full load.
  • Rolling deployments: New JVMs in deployments go through warm-up.

Warm-Up and the Startup Tension​

There is a fundamental trade-off:

  • Fast startup β†’ less compilation β†’ lower peak performance.
  • Aggressive compilation β†’ slower startup β†’ higher peak performance.

Java applications and the JVM must balance these concerns. For long-running server applications, peak performance typically justifies startup overhead.

JIT and Java Performance​

Performance Dimensions Affected by JIT​

DimensionEffect of JIT
ThroughputJIT-compiled code can be significantly faster than interpreted code.
LatencyInlining and optimization reduce operation costs. Compilation itself adds CPU overhead that may increase latency during warm-up.
CPU utilizationHigher CPU use during compilation; lower steady-state CPU use due to optimized code.
Startup timeCompilation adds startup delay; JVM options can trade startup for peak performance.
Memory usageCode cache stores compiled code; optimization may reduce heap allocations (escape analysis).

Performance Lifecycle​

Performance
β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ Steady State (Peak Performance) β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ β”‚ Warm-up Phase β”‚ β”‚
β”‚ β”‚ β”‚ (Gradual compilation & optimization) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ Startup Phase β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ (Interpretation)β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜β†’ Time
β”‚ β”‚ β”‚
Startup Warm-up Peak/Steady
(JIT Compilation) State

Peak Performance Is Not Immediate​

A critical performance insight: peak performance is only available after warm-up. This means:

  • Short-running applications may never reach peak performance.
  • Batch jobs must be long enough to amortize compilation cost.
  • Microbenchmarks must include warm-up iterations.
  • Production services should be warmed up before serving full traffic.

JIT and Garbage Collection​

The JIT compiler and garbage collector interact in significant ways. While they are separate components, the JIT's optimizations can affect GC behavior.

Allocation Optimization​

Escape analysis enables the JIT to avoid heap allocations in many cases:

  • Objects that do not escape can be scalar replaced (fields become local variables).
  • Objects that escape locally may be stack allocated (avoiding heap GC).
  • Reducing allocations β†’ less GC pressure β†’ better overall performance.

Object Lifetime Optimization​

The JIT can sometimes optimize object lifetimes:

  • Short-lived objects may be handled more efficiently.
  • Objects with deterministic lifetimes may be optimized.

Safepoints​

The JIT-generated code must cooperate with GC safepoints:

  • Compiled code includes safepoint checks where GC can pause threads.
  • The JIT must ensure that compiled code cannot hide references (all heap references are trackable during GC).
  • GC safepoints are implemented as part of the execution engine.

Compiler-Generated Code and Managed Memory​

Compiled code interacts with managed memory through:

  • Reads and writes to heap fields.
  • Array accesses with bounds checking (can be optimized in some cases).
  • Method calls with managed references.
  • Synchronization that interacts with the lock implementation.

JIT and Concurrency​

The JIT compiler must respect the Java Memory Model (JMM) and concurrency guarantees.

Synchronization Optimization​

The JIT may optimize synchronization when safe:

  • Lock elision: Eliminating locks for thread-local objects.
  • Lock coarsening: Merging multiple locks.
  • Biased locking: Optimizing for single-threaded lock acquisition.

Important: The JIT cannot eliminate synchronization that would violate JMM guarantees. Synchronization optimizations are only applied when the JVM can prove safety.

volatile and Memory Barriers​

volatile reads and writes impose memory ordering guarantees. The JIT:

  • Preserves the visibility and ordering requirements of volatile.
  • Generates appropriate memory barriers.
  • May optimize volatile access when the JVM can prove a stronger ordering guarantee is already present.

Non-Blocking Code​

The JIT can optimize non-blocking code (e.g., Atomic operations) by using CPU-specific instructions (like CAS) directly, avoiding lock overhead.

Deoptimization and Concurrent Behavior​

Deoptimization is safe in concurrent contexts because:

  • The JVM ensures state is consistent when deoptimizing.
  • All threads see a consistent view of memory.
  • Deoptimization is coordinated with GC safepoints.

JIT Diagnostic and Monitoring Tools​

Understanding JIT behavior in production is essential for diagnosing performance issues. Several tools can help.

Java Flight Recorder (JFR)​

JFR is a built-in profiling and event-collection tool.

  • JIT compilation events: Track compilation start, end, and duration.
  • Code cache events: Monitor code cache usage and flushes.
  • Deoptimization events: Identify when deoptimization occurs.

Java Mission Control (JMC)​

JMC provides a GUI for analyzing JFR recordings.

  • Compilation dashboard
  • Code cache visualization
  • Hot method identification

jcmd​

jcmd is a command-line tool for diagnosing JVM issues.

# Print compilation statistics
jcmd <pid> Compiler.codecache

# Print compiled method details
jcmd <pid> Compiler.codelist

# Print compilation queue
jcmd <pid> Compiler.queue

JITWatch​

JITWatch is a third-party tool that visualizes JIT compilation logs.

  • Parses -XX:+PrintCompilation output.
  • Displays method compilations, inlining, deoptimizations.
  • Helps understand JIT behavior.

PrintCompilation​

A diagnostic flag available in HotSpot:

java -XX:+PrintCompilation MyApplication

Output includes:

  • Compilation timestamp
  • Compilation level (0-4)
  • Method name
  • Compilation status

Note: -XX:+PrintCompilation is a HotSpot diagnostic flag and is not part of the JVM specification. Flags and output formats vary across Java versions and JVM implementations. Use with caution in production.

Other Diagnostic JVM Flags​

HotSpot provides various diagnostic flags:

  • -XX:+PrintInlining: Shows inlining decisions.
  • -XX:+LogCompilation: Writes detailed compilation logs (use with -XX:+UnlockDiagnosticVMOptions).
  • -XX:+PrintCodeCache: Prints code cache usage.

Important: These flags are HotSpot-specific and should only be used for diagnostics or development. They are not stable across Java versions.

JIT Logging and Compilation Diagnostics​

Compilation Logs​

Detailed compilation logs can help diagnose performance issues.

Enabling compilation logging (HotSpot, Java 21+):

java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation MyApplication

This generates an XML file containing:

  • Compilation events
  • Inlining decisions
  • Optimizations applied
  • Deoptimization events

Analyzing Compiled Methods​

You can inspect which methods are compiled and at what level:

jcmd <pid> Compiler.codelist

This lists compiled methods with their compilation levels and sizes.

Detecting Deoptimizations​

JFR events can detect deoptimizations. Look for:

  • Deoptimization events in JFR recordings.
  • Frequent deoptimizations may indicate unstable types or branching.

Interpreting Compilation Levels​

In HotSpot tiered compilation, the compilation level indicates the optimization tier:

  • Level 0: Interpreter
  • Level 1: C1 with no profiling
  • Level 2: C1 with limited profiling
  • Level 3: C1 with full profiling
  • Level 4: C2 (maximally optimized)

Not all JVMs use this numbering, and tier definitions change across versions.

JIT and Benchmarking with JMH​

Accurately measuring the performance of Java code is challenging because the JIT changes behavior over time. The Java Microbenchmark Harness (JMH) is the standard tool for measuring Java performance.

Why JMH Matters​

JMH addresses JIT-related benchmarking pitfalls:

  • Warm-up iterations: JMH runs warm-up iterations to allow the JIT to stabilize before measurements.
  • Measurement iterations: Properly accounting for the warm-up phase.
  • Forks: Each fork is a new JVM instance, ensuring independent measurement.
  • Dead code elimination prevention: JMH can use Blackhole patterns to prevent the JIT from eliminating code that appears unused.
  • Optimization control: JMH provides annotations to control inlining, constant folding, and other optimizations.

The Pitfall of Naive Timing​

long start = System.nanoTime();
doWork();
long elapsed = System.nanoTime() - start;
System.out.println("Time: " + elapsed);

This simple approach is deeply problematic:

  • No warm-up: First execution may be interpreted, not compiled.
  • Dead code elimination: The JIT may determine doWork() has no observable effect and eliminate it.
  • Constant folding: The JIT may compute results at compile time.
  • Unstable results: Execution timing varies dramatically as the JIT compiles.

JMH Best Practices​

  • Use @Warmup and @Measurement annotations to control warm-up.
  • Use @Fork to run benchmarks in separate JVM instances.
  • Use @BenchmarkMode to select measurement mode (throughput, average time, etc.).
  • Use @State to manage benchmark state.

Reference to Performance Engineering​

JMH is covered in detail in the Performance Engineering section's JMH article. For comprehensive benchmarking guidance, refer to that article.

Practical Example: Why JIT Changes Performance​

The following example illustrates the conceptual difference between cold and warm execution. The actual results depend on JVM version, hardware, and workload.

Example Code​

Consider a simple method:

public class JITDemo {
public int compute(int[] data) {
int sum = 0;
for (int i = 0; i < data.length; i++) {
if (data[i] > 0) {
sum += data[i];
}
}
return sum;
}
}

Cold Execution​

On the first invocation:

  • The JVM interprets bytecode instruction by instruction.
  • No profiling data has been collected.
  • No JIT compilation has occurred.
  • Performance is slower due to interpreter overhead.

After Repeated Invocations​

After the method is called many times:

  • Profiling counters exceed the compilation threshold.
  • The JIT compiles the method to native code.
  • The JIT applies optimizations (inlining, loop unrolling, branch prediction).
  • The method runs significantly faster.

Key Insight​

The performance improvement comes not from the source code changing, but from the JVM's adaptive optimization. Repeated execution in a long-running environment yields better performance than the initial execution.

Warm-Up in Practice​

In production:

  • Applications may experience slower performance early in their lifecycle.
  • A deployment must be given time to "warm up."
  • Performance benchmarking must account for warm-up.

Common JIT Misconceptions​

MisconceptionReality
"Java code is always interpreted."False. Hot methods are compiled to native code.
"JIT compiles every method immediately."False. Only hot methods are compiled; others remain interpreted.
"JIT optimization always makes code faster."False. Compilation has overhead, and some optimizations can be wrong for specific workloads.
"JIT eliminates the need for good algorithms."False. JIT cannot fix O(nΒ²) algorithms; good design still matters.
"Java performance is unpredictable because of JIT."Partially true, but JIT behavior is deterministic given stable workloads.
"Native compilation means Java is no longer portable."JIT-generated code is platform-specific, but this happens at runtime. The Java platform retains portability at the bytecode level.
"Deoptimization means the JVM failed."False. Deoptimization is a normal recovery mechanism for speculative optimization.
"More aggressive JIT compilation is always better."False. Aggressive compilation consumes CPU and memory; it can degrade throughput and startup.

JIT Tuning: What Developers Should and Should Not Do​

Developer Focus​

Before considering JVM tuning:

  1. Measure first: Identify actual bottlenecks using profiling tools (JFR, JMC, Async Profiler).
  2. Optimize application code: Often, algorithm or data structure changes yield bigger gains than JVM tuning.
  3. Benchmark properly: Use JMH to measure changes reliably.
  4. Tune the JVM only with evidence: Adjust JVM flags based on data, not intuition.

When JVM Tuning May Be Considered​

  • Code cache size: Increase -XX:ReservedCodeCacheSize if the application compiles many methods and the cache fills.
  • Compilation thread count: Adjust -XX:CICompilerCount for applications with high compilation demand.
  • Tiered compilation thresholds: Advanced tuning for specific workloads (rarely needed).

What Not to Do​

  • Do not set aggressive JIT tuning flags without evidence.
  • Do not copy tuning flags from blogs without understanding your workload.
  • Do not rely on "magic" flag combinations.
  • Do not assume newer is always better (compiler behavior changes across versions).
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ JIT Tuning Best Practices β”‚
β”‚ β”‚
β”‚ 1. Measure application performance in production β”‚
β”‚ 2. Profile to identify hot methods and bottlenecks β”‚
β”‚ 3. Optimize application code first β”‚
β”‚ 4. Benchmark changes with JMH β”‚
β”‚ 5. Only adjust JVM flags if there is clear evidence β”‚
β”‚ 6. Test changes in non-production environments β”‚
β”‚ 7. Monitor performance after changes β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Note: JVM flags and their effects vary across Java versions and JVM implementations. Always test JVM tuning changes thoroughly.

JIT and Java Application Architecture​

JIT behavior has implications for architectural decisions.

Long-Running Applications​

  • Benefit the most from JIT compilation.
  • Peak performance is reached after warm-up.
  • Good fit for server applications, data processing pipelines.

Short-Lived Applications​

  • May not benefit from JIT compilation.
  • Compilation overhead may outweigh benefits.
  • Consider AOT compilation or native images (GraalVM Native Image).

Microservices​

  • Startup time matters: Services restart frequently during deployments.
  • Warm-up cost: Services that restart often pay warm-up costs repeatedly.
  • Traffic patterns: Gradual ramp-up of traffic allows warm-up.

Serverless​

  • Function durations are often short.
  • JIT warm-up may not complete before function ends.
  • Cold starts may dominate performance.

Batch Jobs​

  • Long-running batches: Benefit from JIT.
  • Short-running batches: May not benefit.
  • Data volumes: Larger data volumes β†’ more benefit.

Containerized Applications​

  • Container limits may affect JVM behavior.
  • CPU limits may affect compilation overhead.
  • Memory limits may affect code cache size.

AOT / Native Image Comparison​

  • JIT: Compiles at runtime, adaptive optimization, peak performance.
  • AOT (e.g., GraalVM Native Image): Compiles ahead of time, fast startup, lower peak performance in some workloads, reduced memory footprint.
  • Hybrid: Some Java applications combine both approaches (e.g., AOT for startup, JIT for peak).

The JIT remains the primary performance mechanism for long-running Java applications.

JIT Learning Roadmap​

For developers, architects, and performance engineers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ JIT Learning Roadmap β”‚
β”‚ β”‚
β”‚ 1. Bytecode β”‚
β”‚ Understand how Java source compiles to .class files. β”‚
β”‚ β”‚
β”‚ 2. Interpreter β”‚
β”‚ Understand how the JVM executes bytecode initially. β”‚
β”‚ β”‚
β”‚ 3. Profiling β”‚
β”‚ Learn how the JVM collects runtime execution data. β”‚
β”‚ β”‚
β”‚ 4. Hot Code β”‚
β”‚ Understand how the JVM identifies frequently executed code.β”‚
β”‚ β”‚
β”‚ 5. Tiered Compilation β”‚
β”‚ Learn how the JVM uses multiple compilation tiers. β”‚
β”‚ β”‚
β”‚ 6. Optimization β”‚
β”‚ Understand key optimizations (inlining, escape analysis). β”‚
β”‚ β”‚
β”‚ 7. Machine Code β”‚
β”‚ Learn how machine code is generated for the target CPU. β”‚
β”‚ β”‚
β”‚ 8. Speculative Optimization β”‚
β”‚ Understand how runtime assumptions drive optimization. β”‚
β”‚ β”‚
β”‚ 9. Deoptimization β”‚
β”‚ Learn how the JVM handles invalidated assumptions. β”‚
β”‚ β”‚
β”‚ 10. Performance Analysis β”‚
β”‚ Measure, benchmark, and tune applications. β”‚
β”‚ β”‚
β”‚ 11. Tools β”‚
β”‚ Learn JFR, JMC, JITWatch, and other diagnostic tools. β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Frequently Asked Questions​

What is the JIT compiler in Java?​

The JIT (Just-In-Time) compiler is a component of the JVM that translates Java bytecode into optimized native machine code at runtime, improving performance through profiling-guided optimization.

Does Java compile to machine code?​

Java compiles to bytecode (.class files) at compile time. At runtime, the JVM interprets bytecode and JIT compiles hot methods to machine code. The machine code is generated on the fly, not as part of the static compilation process.

Is Java interpreted or compiled?​

Both. Java source is compiled to bytecode by javac. At runtime, the JVM interprets bytecode. Hot methods are then JIT-compiled to native code. So Java has a hybrid interpretation + compilation execution model.

When does the JVM compile a method?​

The JVM compiles a method when it becomes "hot" β€” when the method invocation count or loop iteration count exceeds a configurable threshold. In tiered compilation, methods go through multiple compilation levels.

What is HotSpot JIT?​

HotSpot JIT is the JIT compiler in the HotSpot/OpenJDK JVM, named for the strategy of identifying "hot spots" (frequently executed code) and compiling only those spots, using adaptive optimization.

What are C1 and C2?​

C1 and C2 are the two primary compilers in the HotSpot JVM. C1 ("Client") compiles code quickly with moderate optimization. C2 ("Server") compiles with aggressive optimization for peak performance. Tiered compilation uses both.

What is tiered compilation?​

Tiered compilation is a HotSpot feature that uses multiple compilation levels. Methods start in the interpreter, then are compiled with C1 (fast), and eventually recompiled with C2 (aggressive) if they become very hot. This balances startup speed and peak performance.

What is JIT warm-up?​

Warm-up is the period during application startup when the JVM interprets bytecode, profiles execution, and compiles hot methods. During warm-up, performance may be lower than the eventual steady-state peak performance.

What is deoptimization?​

Deoptimization is the process by which the JVM "undoes" speculative optimizations when the assumptions used for compilation are violated (e.g., a type profile is wrong). The JVM reverts to interpretation or less-optimized code to maintain correctness.

Why does JIT improve Java performance?​

JIT improves performance by compiling hot methods to native machine code and applying runtime-driven optimizations that are impossible in static compilation, such as inlining based on observed types and eliminating dead code based on runtime profiles.

Can JIT optimization make Java slower?​

Yes, in limited cases. Compilation adds CPU overhead, and if an optimization assumption is frequently wrong, deoptimization costs may outweigh the benefits. Manual JVM tuning can sometimes worsen performance. Always measure.

How can I see which methods are being compiled?​

Use JVM diagnostic flags like -XX:+PrintCompilation (HotSpot) or jcmd <pid> Compiler.codelist. JFR and JMC also provide compilation event visualization.

Next Steps​

Continue Exploring Java Runtime​

  • Java Runtime Architecture: Understand how the JVM is structured.
  • JVM Architecture: Deep dive into JVM internals.
  • Java Memory Model (JMM): Learn about memory visibility and ordering.
  • Java Garbage Collection: Understand memory management.
  • Java Concurrency Model: Explore threading and synchronization.
  • Virtual Threads: Modern lightweight concurrency.

Explore Performance Engineering​

  • Java Performance Engineering: Comprehensive performance guide.
  • Java Profiling Tools: JFR, JMC, Async Profiler.
  • JVM Performance Tuning: Tuning memory, GC, and more.
  • Java Benchmarking with JMH: Precise performance measurement.

Key Takeaway​

The Runtime section explains how the JIT works. The Performance Engineering section focuses on measuring and optimizing real-world performance with this knowledge.

Key Takeaways​

  • Java applications execute bytecode through a combination of interpretation and JIT compilation.
  • Modern JVMs identify frequently executed code and compile it into optimized native machine code.
  • Runtime profiling enables adaptive and speculative optimization, allowing the JIT to perform optimizations impossible in static compilation.
  • Techniques such as inlining, escape analysis, and dead code elimination can significantly improve execution efficiency.
  • Deoptimization allows the JVM to recover when runtime assumptions become invalid, maintaining correctness while enabling aggressive optimization.
  • JIT warm-up is an important consideration for application startup, load testing, and benchmarking.
  • JIT behavior is implementation-dependent, with HotSpot providing one major implementation model; other JVMs may use different strategies.
  • Developers should measure and profile before manually tuning JIT behavior, and apply JVM tuning only when supported by evidence.