JVM
On this page 18
Part I — Foundations · Interview reference
For Java/Kotlin/Scala backends, seniors are expected to reason about memory, GC, classloading, the memory model, and JIT behavior under production load — not just language syntax.
Architecture overview
.java/.kt → compiler → .class bytecode → Classloader → Runtime data areas
↓
Interpreter / JIT (C1/C2/Graal)
↓
GC + native threads
Runtime data areas (conceptual)
| Area | Purpose |
|---|---|
| Heap | Objects / arrays (GC-managed) |
| Metaspace | Class metadata (native, not old PermGen) |
| Java stacks | Per-thread frames (locals, operand stack) |
| PC / native stacks | Execution state; JNI |
| Code cache | JIT-compiled native code |
Memory & GC
Generational hypothesis
Most objects die young → young gen (Eden + Survivor) + old gen. Collect young frequently; full collections cost more.
Collectors you should compare
| Collector | Character | When discussed |
|---|---|---|
| Parallel | Throughput, stop-the-world | Batch |
| G1 | Region-based; balanced; common default | General services |
| ZGC | Ultra-low pause; colored pointers | Large heaps, tight p99 |
| Shenandoah | Low pause | Similar space to ZGC |
Know: pause time vs throughput vs memory footprint tradeoffs; allocation rate as a primary driver of GC pressure.
Signals of GC trouble
- Rising p99 with GC logs showing long pauses
- Allocation rate / object churn from profilers
- Promotion failures / fragmentation (collector-specific)
- Metaspace leaks from dynamic class generation
Tools vocabulary: GC logs (-Xlog:gc*), heap dumps, async-profiler, JFR, jcmd, Eclipse MAT.
Production case study (high volume)
Context: Multi-tenant SaaS API on G1 (32–64GB heaps) serving ~5–20K RPS/instance class; Black Friday allocation rate triples from JSON/ORM churn.
Why seniors care: Allocation rate — not “wrong GC brand” — drives pauses; p99 user checkout failures track GC; oversized heaps without headroom for metaspace/direct/ native blow containers.
Failure / symptom: Rising p99 with -Xlog:gc* showing to-space exhaustion / long remark; heap dump retained by session maps; promotion failure under load test only.
Resolution: Cut allocation (reuse buffers, avoid byte[] copies, DTO churn); tune pause goals only after; consider ZGC when heap large + pause SLO tight; size -Xmx under cgroup limits; MAT dominator trees for leaks.
Seen at / similar to: Netflix / LinkedIn JVM GC writeups; Minecraft/huge-heap ZGC adopters; Shopify JVM services under flash sales.
Java Memory Model (JMM) — interview critical
- Happens-before relates writes to reads across threads
synchronized/ unlock-lock,volatilewrite-read, thread start/join establish ordering- Data races → undefined behavior for Java (broken invariants, infinite loops optimizing assumptions)
- Double-checked locking requires
volatile(since Java 5 memory model fixes)
Link answers to thread safety.
Classloading
- Hierarchy: bootstrap → platform → application; custom loaders for isolation (app servers, plugins)
- Linkage: load → verify → prepare → resolve → initialize
- Leaks: classloader holds classes → static maps → metaspace growth
- SPI / JDBC drivers / shaded jars: version conflicts (“classpath hell”)
Production case study (high volume)
Context: Plugin-style payment processor and a fat “uber” Spring Boot jar in a bank’s card-network connector; redeploys every few hours in lower envs with hot reload experiments.
Why seniors care: Classloader leaks → metaspace OOM after days of traffic; shading conflicts → NoSuchMethodError at peak, not in canaries with different classpath order.
Failure / symptom: Metaspace climb; OutOfMemoryError: Metaspace; intermittent LinkageError after dependency bump.
Resolution: Heap/metaspace dumps; find loader GC roots (static caches); kill hot-reload in prod; align BOM/shading; monitor metaspace used.
Seen at / similar to: App-server plugin eras (WebLogic/JBoss); Kafka Connect connectors; Android multidex historical analogy for loader pain.
JIT & performance
- Hot methods compiled; inlining critical for performance
- Escape analysis → stack allocation / scalar replacement when safe
- Megamorphic call sites inhibit inlining
- Warmup: traffic before measuring peak; primed caches / JIT
Production case study (high volume)
Context: Streaming video origin auth service measures a “performance regression” every deploy for 10 minutes at millions of token checks/day. Why seniors care: Autoscaling on cold pods multiplies warmup pain; seniors distinguish JIT warmup from real code regressions with JFR compiler events. Failure / symptom: New pods flap Ready then fail SLO probes; error budget burn only on scale-out events. Resolution: Pre-warm / minimum warm instances; delay SLO burn alerts until soak; compare compiler profiles pre/post; avoid reflective megamorphic dispatch on the check path. Seen at / similar to: Netflix Warmup / Hollow-style patterns; AWS ALB+ECS scale-out; Twitter JVM performance culture historically.
Threads & runtime
- JVM threads ≈ OS threads (Project Loom virtual threads change scaling model — know the difference)
- Synchronizers: intrinsic locks,
java.util.concurrent, lock-free atomics - Pinning risks with virtual threads + native frames / long
synchronized(awareness)
Java under the hood — locks & virtual threads
| Mechanism | Implementation sketch |
|---|---|
Intrinsic lock (synchronized) | Object header mark word: biased/thin lock → inflated heavyweight monitor on contention; bytecodes monitorenter / monitorexit |
ReentrantLock / j.u.c | AbstractQueuedSynchronizer (AQS): CLH-style wait queue, park/unpark |
| Atomics | CAS via Unsafe historically / VarHandle today; LongAdder stripes to reduce false sharing |
| Platform thread | 1:1 with OS thread (pthread); ~MB-order stack reservation |
| Virtual thread (Loom) | Continuation mounted on a carrier platform thread (default ForkJoinPool); cheap block = unmount continuation |
| Pinning | Carrier stuck if virtual thread blocks inside synchronized or native/JNI frames — prefer ReentrantLock, short critical sections |
Interview depth: explain why virtual threads help blocking IO scale, and why they do not multiply CPU cores. Link to threads and thread safety.
Production case study (high volume)
Context: Migrating a Tomcat/servlet payments API from platform threads to virtual threads to survive Black Friday connection concurrency.
Why seniors care: Pinning inside synchronized JDBC wrappers or legacy monitors reintroduces carrier starvation; unbounded virtual threads still stampede the DB pool.
Failure / symptom: Carriers stuck; JFR pinning events; DB connection wait timeouts despite “millions of cheap threads.”
Resolution: Short critical sections / ReentrantLock; bound concurrency with semaphores matching Hikari max; watch pinning metrics; keep CPU-bound crypto on a fixed pool.
Seen at / similar to: Early Loom adopters in JDK 21+ shops; Helidon/Micronaut/Spring virtual-thread blogs; same lesson as Go: cheap goroutines ≠ free DB.
Operational knobs (speak carefully)
| Concern | Levers (examples) |
|---|---|
| Heap size | -Xms -Xmx (don’t cargo-cult; measure) |
| GC choice | Product defaults + load test |
| OOM | Heap vs Metaspace vs native / direct buffers |
| Direct memory | Netty / NIO; capacity limits |
| Container awareness | cgroup memory limits vs heap |
Senior habit: change one variable, load test, read GC + latency charts.
What interviewers probe
- Explain G1 vs ZGC at tradeoff level.
- Diagnose a memory leak approach (heap dump → dominators → GC roots).
- volatile vs synchronized
- Why equals/hashCode matter for maps; mutability hazards.
- Classpath / shading conflicts in microservices builds.
- Virtual threads: what problem they solve; what they don’t (CPU-bound).
- Finalizers / cleaners — why finalizers are discouraged.
- Warmup vs GC vs pinning — structure a Black Friday incident narrative.
Senior-level expectation: Production war stories structured as symptom → metric → root cause → fix → prevention. Accurate JMM language.
Pitfalls
- Setting max heap = container limit (no room for metaspace, threads, direct, OS)
- Ignoring allocation rate while chasing “GC algorithm”
- Relying on
System.gc() - Synchronizing on public/interned objects (deadlock / DoS)
- Assuming “Java is slow” without profiling (often IO, DB, locks)
- Virtual threads without bounding downstream pools
- Declaring regressions during JIT warmup windows after scale-out