How HotSpot executes code — interpreter, C1/C2 and tiered compilation, hot-spot detection and OSR, profiling-driven speculation, inlining and inline caches (monomorphic/bimorphic/megamorphic, "polymorphic inline caching"), loop unrolling and vectorisation, escape analysis, deoptimisation, the code cache, lock elision/coarsening and the history of biased locking, safepoints, JIT drawbacks and when to avoid warm-up (CDS/AppCDS, AOT caches, native images), and the principle of locality.
Published September 25, 2026
JIT questions test whether you understand why Java is fast after warm-up, and slow before it, and how to reason about micro-benchmarks and latency spikes. Keep the model simple: interpret → profile → compile speculatively → deoptimise when wrong.
Short answer: The Just-In-Time compiler turns frequently executed bytecode into optimised native machine code while the program runs. HotSpot starts by interpreting everything. It counts method invocations and loop back-edges. When a method crosses a threshold, it's queued for background compilation, and later calls jump to the compiled code. Long-running loops are compiled mid-execution, through on-stack replacement (OSR).
Key points to cover:
Short answer:
C1 (the client compiler): compiles quickly, with light optimisations (some inlining, and simple register allocation). It can insert profiling code that gathers type and branch statistics.
C2 (the server compiler): a slow, aggressive, profile-guided optimiser:
It produces the fastest code.
(Graal is an alternative JIT written in Java, available in GraalVM and as Oracle GraalVM's JIT.)
Short answer: Tiered compilation (the default since Java 8) combines both compilers in levels:
A hot method usually goes 0 → 3 → 4. C1 gives fast startup, and C2 gives peak throughput, using the profiles gathered at level 3.
Tuning: -XX:TieredStopAtLevel=1 gives the fastest startup and lowest compile overhead (good for short CLI tools and dev loops). -XX:-TieredCompilation goes straight to C2 after interpreting.
Short answer: A method or loop that runs often enough to be worth compiling. HotSpot keeps per-method counters: invocation count and back-edge (loop iteration) count. When they exceed tier thresholds (scaled by compiler queue load), the method is submitted for compilation at the next tier. A loop whose back-edge counter overflows triggers OSR compilation, which replaces the running interpreted frame with a compiled one.
Short answer: The interpreter and C1 (at level 3) record, per bytecode:
C2 uses this profile to speculate:
These speculations are why Java JIT code can beat static compilers on real workloads. They're also why unrepresentative warm-up (in benchmarks, or with rare code paths) produces misleading results.
Short answer: Inlining replaces a call with the callee's body. It removes call overhead, but, more importantly, it exposes the combined code to other optimisations: escape analysis, constant folding, dead-code elimination, lock elision, loop optimisations. It's called the "mother of all optimisations".
The key limits (HotSpot defaults):
MaxInlineSize) are inlined readily.FreqInlineSize) can be inlined.Key points to cover:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining, or JITWatch.Short answer: An optimisation for virtual calls: cache the receiver type(s) seen at a call site, so dispatch avoids a full lookup. HotSpot works per call site, based on the type profile:
Key points to cover:
Function in a generic stream pipeline, or strategy maps) can become megamorphic. Splitting call sites, or specialising the code, can recover performance. Measure with JMH before and after.Short answer:
int[], float[] and so on) into SIMD instructions (SSE/AVX/NEON), processing 4–16 elements per instruction. For explicit control, use the Vector API (jdk.incubator.vector, still incubating in recent releases).long sum(int[] a) { // a classic unrolled and vectorised loop after C2 compilation
long s = 0;
for (int i = 0; i < a.length; i++) s += a[i];
return s;
}
Common trap: saying "escape analysis allocates objects on the stack". In HotSpot, it removes the allocation entirely through scalar replacement, when the object doesn't escape and is fully inlined.
Short answer: Yes. Compiled code is based on speculative assumptions. When one breaks, the JVM deoptimises: it transfers the running frames back to the interpreter at a safe point, marks the compiled method "not entrant", and later recompiles with the updated profile. Triggers:
Symptoms: "made not entrant" in -XX:+PrintCompilation, and latency hiccups when rare paths first execute in production.
Short answer: The code cache is native memory that stores JIT-compiled code (plus adapters and stubs). Since Java 9 it's segmented: non-method code, profiled (C1) code, and non-profiled (C2) code. The default size is 240 MB with tiered compilation (-XX:ReservedCodeCacheSize). When it's full:
UseCodeCacheFlushing, on by default) tries to evict cold code.Fix: increase ReservedCodeCacheSize for very large applications (heavy frameworks, lots of generated code), and monitor it through JMX, NMT or JFR's CodeCacheFull events.
synchronized blocks at runtime? What happened to biased locking?Short answer:
StringBuffer used inside one method.synchronized blocks on the same object (often in a loop, after inlining) are merged into one lock and unlock, reducing overhead.ObjectMonitor) with a waiter queue, after adaptive spinning.Key points to cover:
synchronized, which removes the main reason to replace synchronized with ReentrantLock for Loom.Short answer: A safepoint is a state in which every Java thread is stopped at a known point, with its stack and object references precisely described (oop maps). That makes it safe for the VM to do global operations:
Compiled code polls for safepoint requests at method returns and loop back-edges.
Key points to cover:
-Xlog:safepoint (the time to reach the safepoint, and the time inside it).Short answer:
What to do instead (rather than "disabling the JIT"):
-Xint (interpreter only) is almost never right: it's 10–50× slower. Use it only to rule out a suspected JIT bug.-XX:TieredStopAtLevel=1 for short-lived tools.Short answer: CDS stores pre-parsed, pre-verified class metadata in an archive file, which is memory-mapped at startup. That skips parsing and verification, and lets several JVMs share the same read-only pages.
-XX:ArchiveClassesAtExit=app.jsa (a dynamic archive, Java 13+), and use it with -XX:SharedArchiveFile=app.jsa.-XX:AOTCacheOutput). Typical startup gains are 30–40%+.Short answer: CPUs are fast only when data is in cache:
In Java:
int[], long[]) are contiguous, and very cache-friendly.ArrayList<Integer> or linked structures are arrays of pointers to scattered objects, which means cache misses and pointer chasing. LinkedList iteration is dramatically slower than ArrayList, even though both are O(n).@Contended or padding).a[i][j], with the inner loop over j).Q: Why do micro-benchmarks written with System.nanoTime() lie?
A: Because of JIT warm-up, OSR compilation of the benchmark loop, dead-code elimination (unused results get optimised away), constant folding, and GC noise. Use JMH: warm-up iterations, forks, blackholes to consume results, and statistical reporting.
Q: What does -XX:+PrintCompilation show?
A: One line per compilation event: timestamp, compile ID, attributes (% for OSR, s for synchronized, ! for exception handlers, n for native), tier level, method name and size, and events like "made not entrant" (deoptimised) or "made zombie".
Q: What is the Vector API?
A: jdk.incubator.vector lets you write explicit SIMD code (for example FloatVector.fromArray(SPECIES, a, i).mul(...)) that compiles to the platform's vector instructions, more reliably than hoping for auto-vectorisation. It's still an incubator module, waiting on Valhalla.
Q: Does final on methods help the JIT inline them?
A: Barely, today. Class hierarchy analysis and type profiles already let C2 inline effectively-monomorphic virtual methods, and deoptimise if that assumption changes. Use final for design reasons, not for speed.