Garbage Collection

15 questions found

How would you determine whether an application is spending an excessive amount of time in garbage collection (GC overhead) relative to actual application work, and what JVM flags or tools help measure and diagnose this?

Advanced
GC overhead ratio (the percentage of total wall-clock time spent paused for garbage collection versus actually executing application code) can be measured via GC logging (-Xlog:gc* in modern JVMs, or the older -verbose:gc/-XX:+PrintGCDetails), which records the duration and frequency of every GC pause, letting you calculate the ratio of cumulative pause time to total elapsed time over a representative period -- the JVM itself has a built-in safety mechanism (the 'GC overhead limit exceeded' condition, part of the default ergonomics) that throws an OutOfMemoryError if the JVM determines it's spending an excessive proportion of time (by default, roughly 98%) doing GC while reclaiming very little memory each time, treating this as effectively equivalent to genuinely running out of memory, since the application is making essentially no real progress; tools like GCViewer or GCEasy can visualize raw GC log output to make sustained high GC overhead immediately apparent.
# Enable detailed GC logging (modern unified logging, Java 9+)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar

# Analyze the resulting gc.log with a tool like GCEasy.io or GCViewer
# to visualize pause frequency, duration, and calculate overall GC overhead percentage
# over the application's runtime
Real-world example A team investigating a service with degraded throughput enables detailed GC logging and discovers via GCEasy analysis that the application was spending nearly 40% of its total runtime paused for garbage collection (far above a healthy single-digit percentage), tracing the root cause to a heap sized too small for the application's actual allocation rate, resolved by both increasing heap size and reducing unnecessary object allocation in a hot code path.

Common follow-ups: What's a reasonable, healthy target GC overhead percentage for a typical production application?;How does the 'GC overhead limit exceeded' OutOfMemoryError specifically differ from a genuine heap-exhaustion OutOfMemoryError?

Diagnostics & Performance;Incident Response

What does it mean for an object to be 'eligible for garbage collection' in terms of reachability from GC roots?

Beginner
An object becomes eligible for garbage collection once it's no longer reachable through any chain of references starting from a GC root -- GC roots include active local variables and parameters on any thread's current stack frames, static fields, and JNI references -- if you trace every reference chain starting from these roots and a particular object is never encountered, it's unreachable and therefore eligible for collection, regardless of how many objects might still reference EACH OTHER in an isolated island with no path back to any GC root (a scenario reference counting alone would fail to collect, but reachability-based tracing correctly identifies as garbage).
public void example() {
    Object a = new Object();
    Object b = new Object();
    a = null;  // the original object 'a' pointed to now has NO reachable reference -- eligible for GC
    b = null;  // same for the object 'b' pointed to
    // Both original objects are now unreachable from any GC root and will eventually be collected
}
Real-world example A circular reference between two objects (each holding a reference to the other) that are both otherwise unreachable from any GC root is still correctly identified as garbage by Java's reachability-based collector, unlike a simpler reference-counting scheme (used by some other languages) which would incorrectly consider each object still 'referenced' by the other and fail to collect this circular island.

Common follow-ups: Why does Java use reachability tracing rather than simple reference counting, given reference counting is conceptually simpler?;What specifically counts as a GC root beyond local variables and static fields?

JVM JRE & Memory;Collections Framework

How would you use jstat to monitor GC activity and heap generation sizes for a running JVM process in real time, without needing a full profiling tool?

Intermediate
jstat -gcutil <pid> <interval> prints periodic snapshots of each heap generation's utilization percentage (young generation survivor spaces, Eden, old generation, and metaspace) along with cumulative GC event counts and total time spent in young/full GC, providing a lightweight, always-available (bundled with the JDK, no extra setup) way to observe GC behavior and heap pressure trends in real time directly from the command line, useful for quick production diagnostics without needing to attach a heavier profiling tool.
# Print GC utilization stats every 1 second, indefinitely
jstat -gcutil <pid> 1000

# Sample output columns include:
# S0/S1 (survivor space %), E (Eden %), O (old gen %), M (metaspace %), YGC/YGCT (young GC count/time), FGC/FGCT (full GC count/time)
Real-world example An on-call engineer investigating a suspected memory pressure issue in a running production JVM runs jstat -gcutil directly against the process ID, immediately observing the old generation utilization climbing steadily toward 100% between full GCs, a lightweight first diagnostic step before reaching for a heavier tool like a full heap dump analysis.

Common follow-ups: How would you interpret a pattern of frequent full GCs with the old generation utilization dropping only slightly after each one?;What's the difference between jstat's snapshot-based monitoring and continuous GC logging via -Xlog:gc?

Diagnostics & Performance;Incident Response

How does Metaspace (replacing PermGen since Java 8) store class metadata, and what specific configuration and failure modes differ from the old PermGen model?

Advanced
Metaspace stores class metadata (loaded class definitions, method bytecode, constant pools) in native memory OUTSIDE the regular Java heap (unlike its predecessor PermGen, which was part of the heap itself and prone to a notorious OutOfMemoryError: PermGen space under class-loading-heavy scenarios like frequent hot redeployment) -- since Metaspace uses native memory, it can grow dynamically up to the available system memory by default (unless explicitly capped via -XX:MaxMetaspaceSize), which mostly eliminated PermGen's most common failure mode, though a genuine classloader leak (classes/classloaders never becoming garbage collectible due to lingering references) can still exhaust Metaspace and cause an analogous OutOfMemoryError: Metaspace, just with a much higher practical ceiling before hitting it.
# Explicitly cap Metaspace size (otherwise limited only by available native/system memory)
java -XX:MaxMetaspaceSize=256m -jar app.jar

# Monitor current Metaspace usage
jstat -gcutil <pid> | awk '{print $6}'  # M column shows metaspace utilization %
Real-world example An application server performing frequent hot-redeployments of web applications (each redeployment potentially leaving behind an unreleased classloader if not implemented carefully) previously hit OutOfMemoryError: PermGen space regularly under the old model, but after upgrading to a modern JVM with Metaspace, the same underlying classloader leak now takes dramatically longer to actually exhaust available memory, buying more time to properly fix the root leak.

Common follow-ups: Why does a classloader leak still cause an OutOfMemoryError under Metaspace despite it not being part of the regular heap?;What's the operational trade-off of leaving MaxMetaspaceSize unbounded (default) versus explicitly capping it in production?

Class Loading & Bytecode Verification;JVM JRE & Memory

What is the difference between a stop-the-world (STW) GC pause and a concurrent GC phase, and why can't garbage collection be made entirely concurrent with zero pauses?

Intermediate
A stop-the-world pause completely suspends ALL application threads while the GC performs certain operations that require a stable, unchanging view of the heap and all object references (most notably, the initial root-scanning phase that identifies the starting set of GC roots, and typically the final compaction/relocation phase that physically moves surviving objects to defragment memory) -- while modern collectors like G1/ZGC/Shenandoah perform the bulk of marking and even much of the relocation work CONCURRENTLY alongside running application threads (using techniques like write barriers and read barriers to handle concurrent mutation safely), completely eliminating STW pauses remains extremely difficult since certain coordination points (like establishing a consistent initial root set, or briefly pausing to finalize a relocation) fundamentally benefit from or require a moment of full synchronization across all threads, which is why even the most advanced low-latency collectors still have brief (though very short) STW pauses rather than zero pauses entirely.
# GC log output distinguishing pause types (illustrative, actual format varies by collector/JVM version)
# [gc,phases] GC(42) Pause Young (Normal) (G1 Evacuation Pause) 45M->12M(128M) 8.2ms  <- STW pause
# [gc,phases] GC(42) Concurrent Mark Cycle  <- runs alongside application threads, no full pause
Real-world example A latency-sensitive application monitoring its GC logs observes that even with ZGC configured, brief sub-millisecond STW pauses still occur periodically for root-scanning coordination, confirming that 'concurrent' garbage collection significantly reduces but doesn't entirely eliminate stop-the-world pauses, an important distinction when setting realistic latency expectations.

Common follow-ups: What specific GC phases are the hardest to make fully concurrent, and why?;How do read barriers (used by ZGC) differ from write barriers in what concurrent mutation problem they solve?

Diagnostics & Performance;Concurrency & Threads

Showing 11–15 of 15