JVM Internals & Memory
JIT Compilation
- Tiered: interpreter → C1 (profiled) → C2 at ~10k invocations; loops trigger on-stack replacement
- Inlining is the gateway optimization — it unlocks everything else
- Speculation + deoptimization: monomorphic call sites devirtualize; a new receiver type deopts back
- Escape analysis: non-escaping objects become stack scalars — allocation eliminated
- Warm-up is real: benchmark and load-test only after the JIT settles (Microbenchmarking)
- Code cache full = compilation silently stops — monitor it
Why late compilation wins (Optimizing Java ch. 10): by the time C2 compiles a method it has thousands of profile samples — which branch is 99% taken, which instanceof always passes, that a virtual call always lands in one implementation. It compiles the observed program, guards the assumptions cheaply, and deoptimizes (falls back to the interpreter, recompiles) if reality changes. Static compilers must be correct for all inputs; the JIT only for the ones that happen, and it gets to change its mind.
$ java -XX:+PrintCompilation App | grep MyService
312 45 3 com.acme.MyService::price (86 bytes) // tier 3 (C1+profile)
899 102 4 com.acme.MyService::price (86 bytes) // tier 4 (C2)
903 45 3 com.acme.MyService::price (86 bytes) made not entrant // replaced
// deeper: -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining, or JITWatch on -XX:+LogCompilationInlining replaces a call with the callee's body — killing call overhead and, critically, letting optimizations see across method boundaries: constants propagate, allocations become scalars (escape analysis), locks on non-escaping objects vanish (lock elision), bounds checks hoist out of loops, and loops vectorize. Small methods inline best (-XX:MaxInlineSize ~35 bytecodes, hot methods up to FreqInlineSize ~325) — one more reason the "many small methods" style is fast, not slow, on the JVM.