JVM Performance — Questions with Complete Answers

The JD says "deep Java expertise (concurrent programming, JVM internals, GC behavior, heap analysis)". These are the questions that test it, with answers grounded in the Phase 08 labs.

Table of Contents


Garbage collection

Q: Compare G1, ZGC, and Parallel GC. When would you pick each for a data-infrastructure JVM?

Parallel GC maximizes throughput with stop-the-world collections — right for batch jobs (Spark executors doing CPU-bound work) where a 500 ms pause is irrelevant. G1 targets predictable pauses by collecting region-by-region with a pause-time goal — the default general-purpose choice for services and most brokers. ZGC gives sub-millisecond pauses via colored pointers and concurrent relocation at some throughput and memory cost — right for latency-SLO services with large heaps (100 GB+). For Kafka brokers, note that the page cache, not the heap, does the heavy lifting — keep heaps modest (~6 GB) regardless of collector.

Q: What is allocation rate and why does it matter more than heap size?

Bytes allocated per second drives the frequency of young collections; most "GC problems" in data systems are allocation-rate problems (per-record garbage in hot loops: boxing, iterator churn, string concatenation, temporary arrays). Halving allocation rate halves young-GC frequency without touching the heap. Phase 08 Lab 02 measures exactly this: boxing (Long vs long accumulators) and stream-vs-loop allocation profiles via JFR/-verbose:gc.

Q: A Spark executor spends 40% of time in GC. What do you look at?

In order: (1) allocation rate and what's allocating — JFR allocation profiling or async profiler alloc mode; usual suspects are wide-row deserialization, UDF object churn, and collect-like operators; (2) memory config — executor memory vs spark.memory.fraction, whether execution spills are thrashing; (3) data skew creating one giant partition (fix the partitioning, not the GC); (4) only then GC tuning — bigger young gen for batch, or G1 region size for humongous allocations (records > 16 MB). Saying "increase the heap" first is a junior answer; the heap often just makes pauses longer.


JIT and benchmarking

Q: Why do naive Java microbenchmarks lie? Name the specific JIT effects.

Dead-code elimination (unused results delete your benchmark body — counter with JMH Blackhole), constant folding (compile-time-known inputs fold away — use @State fields), loop unrolling combined with OSR producing unrepresentative compilation, warmup (first N calls run interpreted/C1; steady state is C2), inlining differences between the benchmark harness and real call sites, and profile pollution (a megamorphic call site in one test deoptimizes another's monomorphic fast path). JMH exists to control all of these — forked JVMs, warmup iterations, blackholes — which is Phase 08 Lab 01.

Q: What is a safepoint and how does it bite production systems?

A point where all threads are at known states so the VM can do stop-the-world work (GC phases, deoptimization, biased-lock revocation, thread dumps). Threads poll for safepoint requests; a thread in a long counted loop without a poll (classic: int-indexed hot loop) delays everyone — time-to-safepoint spikes show up as mysterious tail latency even when "GC pauses" look short. Diagnose with -Xlog:safepoint.


Memory model and concurrency

Q: What does volatile guarantee, and what does it not?

Visibility (a read sees the latest write, via happens-before on the volatile variable) and ordering (no reordering of surrounding accesses across the volatile access). It does not make compound operations atomic — count++ on a volatile is still a lost-update race. For counters use AtomicLong (CAS) or LongAdder (striped cells); Phase 08 Lab 03 benchmarks why: under contention, CAS retry storms make AtomicLong degrade while LongAdder scales, at the cost of a more expensive sum().

Q: synchronized vs ReentrantLock vs StampedLock — when does each win?

synchronized: simplest, JIT-friendly (lock elision, coarsening, biased locking historically), right default for uncontended or briefly-held locks. ReentrantLock: tryLock with timeout, interruptible acquisition, fairness, multiple conditions — choose it when you need those features, not for speed. StampedLock: optimistic reads make read-mostly structures very fast, but it's non-reentrant and easy to misuse. The deeper answer: at high contention the win is not having the lock — striping (LongAdder, ConcurrentHashMap), immutability, or per-thread accumulation then merge.

Q: Explain a memory leak pattern specific to long-running JVM data services.

Unbounded caches keyed by something with unstable hashCode/equals; listener/callback registration without deregistration; ThreadLocals on pooled threads (the pool keeps the thread, the thread keeps the value); classloader leaks on redeploy (a static somewhere holds an instance whose class came from the old loader); and accumulating metrics registries with per-entity tags of unbounded cardinality. Heap-dump triage: dominator tree in MAT/jhat → biggest retained sets → ask "what owns this and why is it still reachable?"


Heap analysis war stories

Use these as templates — replace with your own lab numbers.

  • The boxing regression: a refactor changed a hot aggregation from long to Long; allocation rate ×6, young GC every 300 ms. Found via JFR allocation profile; fixed with a primitive accumulator; wrote a JMH benchmark into CI to pin it. (Phase 08 Lab 02 is this.)
  • The contended counter: metrics singleton with AtomicLong.incrementAndGet() on every record; 32 cores, throughput plateaued at 8. Async-profiler showed 40% cycles in CAS retries; LongAdder restored linear scaling. (Phase 08 Lab 03 reproduces it.)
  • The "clever" partitioner: bit-twiddled hash that was fast and wrong — skewed 70% of keys to 3 partitions. Chi-square test in the review caught it. (Phase 05 Lab 03 / Phase 07 Lab 02.)

References