Garbage Collection
Generational collection, System.gc(), low-latency collectors, reference types, memory leaks despite a GC, and GC tuning and diagnosis.
10 questions
JuniorTheoryVery commonWhat is the difference between the young and old generation in the JVM heap?
What is the difference between the young and old generation in the JVM heap?
Most objects die young — the weak generational hypothesis. The young gen (eden and two survivor spaces) is swept by frequent minor GCs that copy the few survivors; those are promoted to the old gen, collected rarely but expensively.
Common mistakes
- ✗Thinking every object lives about the same length, so generations add nothing
- ✗Believing the old generation is collected as often and as cheaply as the young
- ✗Assuming new objects are allocated straight into the old generation
Follow-up questions
- →What triggers an object's promotion from the survivor spaces to the old gen?
- →Why does the young generation use two survivor spaces rather than one?
JuniorTheoryVery commonWhat is garbage collection (GC) in Java and what problem does it solve?
What is garbage collection (GC) in Java and what problem does it solve?
Garbage collection is the JVM automatically reclaiming heap objects that are no longer reachable from any live reference. It frees the developer from manual memory management — there is no free or delete — which prevents most leaks and dangling pointers. The trade-off is that collection timing is non-deterministic: you cannot predict exactly when an object is reclaimed.
Common mistakes
- ✗Assuming
GCruns deterministically the instant an object becomes unreachable - ✗Thinking automatic memory management makes
heapleaks impossible in Java - ✗Looking for a manual
free/delete, which Java deliberately does not provide
Follow-up questions
- →If references are still held, can an object still leak despite garbage collection?
- →How does the collector decide an object is unreachable rather than just unused?
MiddleDebuggingCommonA static cache grows without bound despite the garbage collector
A static cache grows without bound despite the garbage collector
The static final map is a GC root holding a strong reference to every Session ever parsed, so no entry ever becomes unreachable and the collector cannot reclaim it — a leak by reachability, not a GC bug. Fix it by bounding the cache: an LRU via LinkedHashMap.removeEldestEntry, or a WeakHashMap whose entries clear once no strong reference to the key remains.
Common mistakes
- ✗Believing a garbage collector makes a memory leak impossible in Java
- ✗Blaming the collector or the generation sizing when the object is still reachable
- ✗Treating
System.gc()or a bigger heap as a fix for an unbounded cache
Follow-up questions
- →How would a heap dump show which GC root is retaining the sessions?
- →When is a
WeakHashMapthe wrong cache and a bounded LRU the right one?
MiddleTheoryCommonWhat is the difference between strong, soft, weak, and phantom references in Java?
What is the difference between strong, soft, weak, and phantom references in Java?
A strong reference — the default — keeps its object reachable and blocks collection. A SoftReference is cleared only under memory pressure, so it fits memory-sensitive caches. A WeakReference is cleared at the next GC once no strong reference remains, as with WeakHashMap keys. A PhantomReference is never followable (get() always returns null); it only enqueues on a ReferenceQueue after collection, a safe post-mortem cleanup hook replacing finalize().
Common mistakes
- ✗Assuming a
WeakReferenceis cleared only under memory pressure like aSoftReference - ✗Expecting
PhantomReference.get()to return the object rather than alwaysnull - ✗Treating soft and weak references as interchangeable for caching
Follow-up questions
- →When would a
SoftReferencecache still trigger anOutOfMemoryError? - →Why is a
PhantomReferenceplus aReferenceQueuesafer thanfinalize()?
JuniorTheoryOccasionalDoes calling System.gc() guarantee that a garbage collection runs?
Does calling System.gc() guarantee that a garbage collection runs?
No — System.gc() is only a suggestion. The JVM may ignore it, and -XX:+DisableExplicitGC makes it a no-op. Calling it in app code typically forces an expensive full GC that hurts throughput; it does not free memory or fix a leak.
Common mistakes
- ✗Believing
System.gc()forces an immediate, guaranteed collection - ✗Calling it in production to free memory or clear a suspected leak
- ✗Assuming it is cheap rather than usually a full stop-the-world GC
Follow-up questions
- →Why can
-XX:+DisableExplicitGCbe a useful production flag? - →If not
System.gc(), how should you address a suspected memory leak?
MiddleDebuggingOccasionalDiagnose from a jstat readout why a service spends most of its time in GC
Diagnose from a jstat readout why a service spends most of its time in GC
Old-gen occupancy O stays above 97% and never drops after a full GC, while FGC and FGCT climb steeply — back-to-back full collections that reclaim almost nothing. That is the signature of a retained live set (a leak), not of an undersized young generation. Confirm with a heap dump (jmap -dump) read in Eclipse MAT or VisualVM, whose dominator tree names the retaining GC root, plus -Xlog:gc* or JFR for the pause history.
Common mistakes
- ✗Reading a high old-gen occupancy as healthy rather than as a retained live set
- ✗Tuning heap flags before confirming what is retained, with a heap dump
- ✗Assuming a rising full-GC count is normal for any long-running service
Follow-up questions
- →What does a dominator tree in Eclipse MAT tell you that
jstatcannot? - →How would the same readout look if the young generation really were too small?
SeniorTheoryOccasionalWhat garbage collectors (GC) does the JVM offer, and how do they differ?
What garbage collectors (GC) does the JVM offer, and how do they differ?
Serial uses one thread, fine for small heaps. Parallel uses many threads to maximize throughput. CMS was concurrent but is now removed. G1 (garbage-first) is the default — region-based, balancing throughput against predictable pauses. ZGC and Shenandoah target very low, heap-size-independent pause times. The core trade-off is throughput versus pause time: more concurrent collectors cut pauses at some throughput cost.
Common mistakes
- ✗Treating one collector as strictly best instead of a throughput-versus-pause trade-off
- ✗Naming CMS as a current option, when it has been removed from modern JVMs
- ✗Assuming every collector is fully stop-the-world rather than concurrent or region-based
Follow-up questions
- →How does
G1's region-based layout let it meet a target pause-time goal? - →What technique lets
ZGCkeep pauses sub-millisecond even on multi-terabyte heaps?
MiddlePerformanceRareHow does the low-pause collector ZGC keep stop-the-world (STW) pauses sub-millisecond?
How does the low-pause collector ZGC keep stop-the-world (STW) pauses sub-millisecond?
ZGC does marking, relocation, and reference remapping concurrently with the running application. Coloured pointers keep GC state in unused address bits, and a load barrier on every reference read lazily remaps a stale pointer. Only brief root scans are stop-the-world, so pause time tracks the root-set size, not the heap size.
Common mistakes
- ✗Thinking
ZGCis justG1with more GC threads rather than a concurrent collector - ✗Assuming
ZGCpause time grows with heap size like the older collectors - ✗Confusing the load barrier on reads with a write barrier, or assuming no barrier at all
Follow-up questions
- →What work does
ZGCstill have to do inside a stop-the-world pause? - →Why does
ZGCtrade away some throughput for its sub-millisecond pauses?
SeniorDebuggingRareDiagnose growing stop-the-world (STW) GC pauses in a production service
Diagnose growing stop-the-world (STW) GC pauses in a production service
G1 Humongous Allocation marks objects larger than half a region: they are allocated straight into the old gen and collected poorly. They fragment the heap until evacuation has no free region left (To-space exhausted), so G1 falls back to a full stop-the-world compaction — the multi-second pauses. No pause-time goal can survive a fallback full GC. Fix the allocation itself, enlarge -XX:G1HeapRegionSize, and lower InitiatingHeapOccupancyPercent so the concurrent cycle starts earlier.
Common mistakes
- ✗Reading
To-space exhaustedas a survivor-sizing issue rather than a lack of free regions - ✗Raising the pause-time goal instead of removing the oversized allocations behind it
- ✗Assuming a rising post-collection occupancy always means an ordinary leak
Follow-up questions
- →Why does an object larger than half a
G1region skip the young generation? - →Would the low-pause collector
ZGChave avoided this failure, and at what cost?
SeniorDebuggingRareInvestigate a slow memory leak in a Spring Boot service from a heap histogram
Investigate a slow memory leak in a Spring Boot service from a heap histogram
The histogram shows AuditEntry — and the byte[]/String it retains — growing without bound in lockstep with request volume, held through HashMap$Node. The audit bean accumulates one entry per request and never evicts. A Spring singleton bean lives as long as the application context, so everything it collects stays reachable and the GC is correct not to free it. Confirm the retaining path in a heap dump's dominator tree, then bound the buffer with a fixed size or an eviction policy.
Common mistakes
- ✗Treating a singleton bean's collection as short-lived, request-scoped state
- ✗Raising
-Xmxor swapping the collector to buy time instead of finding the retaining root - ✗Reading the largest histogram row (
byte[]) as the leak instead of what retains it
Follow-up questions
- →Why does a dominator tree, not the histogram, identify the retaining owner?
- →How would you bound the buffer without losing the audit records the admin page needs?