JIT Compilation
The JVM starts by interpreting bytecode. The JIT (Just-In-Time) compiler monitors hot methods (those called frequently) and compiles them to native machine code at runtime, dramatically improving throughput.
JIT = a chef who watches which dishes customers order most, then pre-preps those ingredients. Cold start is the first morning rush; warm-up is the chef learning the popular orders.
HotSpot uses two JIT compilers: C1 (client compiler) — fast compilation with light optimization, used for startup; C2 (server compiler) — aggressive optimization (inlining, escape analysis, loop unrolling) for peak throughput. Tiered compilation (default since Java 7u40) uses both: code starts in C1 and graduates to C2 based on invocation and loop-back counters. The JVM can deoptimize (bail out to interpreter) if an optimistic assumption (e.g., monomorphic call site) is violated.
Escape analysis allows the JIT to allocate short-lived objects on the stack instead of the heap (scalar replacement), reducing GC pressure. Inline caches and polymorphic inline caches (PICs) accelerate virtual dispatch. Profile-guided optimization means JIT output adapts to actual runtime behavior — which is why JVM performance benchmarks must account for warm-up time. GraalVM's JIT (available via -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler) is written in Java itself and can produce better peak throughput for some workloads. AOT compilation (GraalVM native-image) trades peak throughput and dynamic features for instant startup.
HotSpot uses tiered compilation: bytecode is first interpreted, then compiled by C1 for fast startup, and finally by C2 with aggressive optimizations (inlining, escape analysis) for peak throughput. The JIT can deoptimize and fall back to interpreted mode if optimistic assumptions — like a call site being monomorphic — are violated at runtime. This is why JVM applications exhibit a warm-up curve and why load testing must include ramp-up time.
Microbenchmarks that don't use JMH (Java Microbenchmark Harness) are often invalidated by JIT dead-code elimination or incomplete warm-up — the JVM may optimize away your benchmark entirely if results are unused.