Skip to content

JVM, memory and GC

A strength because of Prayog's GC story. Get the basics right, then tell the story.

Describe the JVM memory areas.
  • Heap: objects; shared by all threads; garbage collected. Divided into young (eden + survivors) and old generations in generational collectors.
  • Stack (per thread): frames with local variables and partial results; StackOverflowError on deep recursion.
  • Metaspace (native memory, Java 8+, replaced PermGen): class metadata.
  • Code cache: JIT-compiled code.
  • Direct / off-heap memory: ByteBuffer.allocateDirect, memory-mapped files (Prayog's journal), not managed by GC.
How does garbage collection work? What is generational GC?

The GC finds objects no longer reachable from GC roots (thread stacks, static fields, JNI refs) and frees them. Generational hypothesis: most objects die young. New objects go to eden; a minor GC copies survivors to a survivor space; objects that survive enough GCs are promoted to the old generation, collected less often (major/ mixed GC). Copying young-gen collection is cheap because it touches only live objects.

Compare Serial, Parallel, G1, ZGC, Shenandoah.
Collector How Good for
Serial One thread, stop-the-world Tiny heaps, single CPU; the JVM's default in small containers
Parallel Many threads, stop-the-world Throughput, batch jobs
G1 (default since 9) Heap in regions, collects garbage-first regions, pause-time target (MaxGCPauseMillis, default 200 ms) General servers
ZGC (generational in 21) Concurrent, coloured pointers and load barriers, sub-millisecond pauses Large heaps, latency-sensitive; needs headroom
Shenandoah Concurrent compaction Similar goals to ZGC
What is a stop-the-world pause and why does it matter?

All application threads stop while the GC does some work. For a latency-sensitive service, a 100 ms pause is a 100 ms latency spike for every request in flight: it shows in p99/p99.9, not in averages.

How do you diagnose a memory leak or an OutOfMemoryError?

Find which OOM: Java heap space, Metaspace, GC overhead limit exceeded, Direct buffer memory, unable to create native thread. Enable -XX:+HeapDumpOnOutOfMemoryError, analyse the dump with Eclipse MAT or VisualVM (dominator tree, biggest retained sizes). Watch heap after full GC over time: a rising floor is a leak. Common causes: unbounded caches/maps, static collections, ThreadLocal in pools, listeners never removed, unclosed resources.

Prayog link: the journal archiver held two 64 MiB copies in memory and hit OutOfMemoryError under a 96 MiB heap; fixed by comparing in 64 KiB chunks (failure 4).

How do you diagnose high latency or high CPU in a Java service?
  1. Metrics first: latency percentiles, GC pause time, CPU, thread pool queue sizes.
  2. GC logs (-Xlog:gc*): are pauses lining up with the spikes?
  3. Thread dumps (jstack, several a few seconds apart): blocked threads, lock contention, hot loops.
  4. Profile: JFR (jcmd <pid> JFR.start) for CPU, allocation and lock profiling; async-profiler for flame graphs.
  5. Fix the cause, then measure again.
What is JIT compilation? Why warm up benchmarks?

The JVM starts by interpreting bytecode, profiles hot methods, and compiles them to native code (C1 quickly, C2 with heavy optimisations: inlining, escape analysis, loop unrolling). Until then code is slower, so benchmarks need warm-up iterations. That's why I used JMH, which handles warm-up, forks and dead-code elimination (Blackhole).

What is escape analysis?

The JIT checks whether an object escapes a method/thread. If not, it can allocate it on the stack or replace it with scalars (no heap allocation), and remove locks on it.

How do containers affect the JVM?

Since Java 10 (backported to 8u191), the JVM reads cgroup limits for CPU and memory. Default max heap is 25% of the container memory; set -XX:MaxRAMPercentage=75 to use more. With under 2 CPUs or under ~1792 MB, the JVM considers itself "client-class" and picks Serial GC. Prayog hit exactly this: a 768 MiB container picked Serial, and pauses stopped the matching thread.

Key JVM flags you know?

-Xms/-Xmx (heap), -XX:MaxRAMPercentage, -XX:+UseG1GC / UseZGC, -XX:MaxGCPauseMillis, -Xlog:gc*, -XX:+HeapDumpOnOutOfMemoryError, -XX:StartFlightRecording, -XX:+AlwaysPreTouch (touch heap pages at start so first use doesn't page-fault; Prayog used it for repeatable benchmarks).

Strong, soft, weak, phantom references?

Strong: normal, never collected while reachable. Soft: collected only under memory pressure (caches). Weak: collected at next GC once only weakly reachable (WeakHashMap, canonicalising maps). Phantom: for cleanup after collection (Cleaner).

How does class loading work?

Loading → linking (verify, prepare, resolve) → initialisation. Parent-delegation: application loader asks platform loader asks bootstrap loader first, so core classes can't be replaced. Static initialisers run once, thread-safely, on first active use (that's why the holder-idiom singleton works).