Skip to content

Garbage Collection Interview Questions & Answers

20 questions Updated 2026-06-20 Share:

Java garbage collection interview questions — GC algorithms, Serial/Parallel/G1/ZGC/Shenandoah collectors, stop-the-world pauses, Minor vs Full GC, GC tuning flags, GC logs, finalization, and avoiding common GC anti-patterns.

Read the in-depth guideJava Garbage Collection Deep Dive — G1, ZGC, Tuning, and Avoiding GC Pauses(opens in new tab)
20 of 20

Garbage collection (GC) is the automatic process by which the JVM identifies and reclaims heap memory occupied by objects that are no longer reachable from any GC root (live thread, static field, JNI reference). Without GC, developers would have to free() objects manually — the source of dangling-pointer and double-free bugs in languages like C.

void createAndForget() {
    String s = new String("temporary"); // allocated on heap
}   // s goes out of scope → no reference remains → object is unreachable
    // GC will reclaim it on the next collection cycle

Rule of thumb: GC removes unreachable objects; it does NOT remove objects that are still referenced even if you'll never use them again — that's a memory leak.

Mark-and-sweep runs in two phases:

  1. Mark — start from every GC root and trace all reachable objects, setting a "live" bit on each one.
  2. Sweep — scan the entire heap; reclaim any object without the live bit set; clear live bits for next cycle.
Heap before GC:   [A (live)] [B (dead)] [C (live)] [D (dead)]
After mark:       [A ✓]      [B  ]      [C ✓]      [D  ]
After sweep:      [A]        [free]      [C]         [free]

The downside is fragmentation — freed slots are scattered, making large allocations hard. Modern collectors add a compaction phase to slide live objects together and reset the allocation pointer.

Rule of thumb: mark-and-sweep tells you what to collect; compaction tells you how to make the freed space usable.

The weak generational hypothesis — most objects die young — means that collecting only the small, short-lived Young Generation most of the time is far more efficient than scanning the whole heap every GC cycle.

GC type Region swept Frequency Typical pause
Minor GC Young Gen (Eden + Survivors) frequent ms
Major/Full GC Entire heap infrequent tens of ms → seconds
// Allocation pattern that plays nicely with generational GC:
void processRequest() {
    List<Item> results = new ArrayList<>(); // short-lived; dies in Eden
    for (Item i : fetch()) results.add(process(i));
    return results;
}   // results unreachable after method returns → collected cheaply

Rule of thumb: keep temporary objects truly temporary — objects that escape into long-lived caches defeat generational GC and inflate Old Gen.

The Serial GC (-XX:+UseSerialGC) uses a single thread for both Minor and Major GC. The entire JVM stops during collection (stop-the-world). It is the simplest collector and has the lowest overhead per collection, but pauses are longest when the heap is large.

java -XX:+UseSerialGC -Xmx256m MyApp

It is appropriate for:

  • Small heaps (< ~256 MB)
  • Single-core machines or environments with very few CPUs
  • Embedded or batch workloads where pause time is not a concern

Rule of thumb: Serial GC is correct for tiny, single-threaded CLI tools; any latency-sensitive server should use a concurrent collector.

The Parallel GC (also called Throughput Collector, -XX:+UseParallelGC, default before Java 9) uses multiple threads for both Minor and Full GC, cutting pause times compared to Serial. The application still stops during GC, but the work is divided across cores.

java -XX:+UseParallelGC -XX:ParallelGCThreads=8 MyApp

It maximises throughput (fraction of time not spent in GC) at the cost of longer individual pauses than concurrent collectors. A good fit for batch jobs that care more about total work done than per-request latency.

Rule of thumb: Parallel GC = maximum throughput, accept the pauses; choose a concurrent collector (G1, ZGC) when latency matters.

Concurrent Mark-Sweep (CMS) (-XX:+UseConcMarkSweepGC) ran most of its work concurrently with the application, greatly reducing Old Gen pause times. It had two short stop-the-world phases (initial mark, remark) and long concurrent phases for marking and sweeping.

Its flaws:

  • No compaction — memory fragmentation grew over time, eventually causing a "concurrent mode failure" that triggered a full stop-the-world compacting collection.
  • High CPU overhead from running GC threads alongside the app.
  • Deprecated in Java 9, removed in Java 14.
# No longer valid in Java 14+:
# java -XX:+UseConcMarkSweepGC ...

Rule of thumb: CMS is a historical reference; in interviews, position G1 as its replacement and ZGC/Shenandoah as the next evolution.

G1 (Garbage First) is the default GC since Java 9. It divides the heap into equal-sized regions (1–32 MB each) instead of fixed Eden/Survivor/ Old Gen areas. Each region is labelled dynamically.

G1 collects the regions with the most garbage first (hence "Garbage First"), providing predictable pause targets via -XX:MaxGCPauseMillis (default 200 ms). It runs most work concurrently and compacts incrementally.

java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 MyApp

Key G1 concepts:

  • Humongous regions — objects > 50 % of region size get their own contiguous region(s); avoid creating many large short-lived objects.
  • Mixed GC — after a concurrent marking cycle, G1 collects Young Gen plus the most garbage-dense Old Gen regions together.

Rule of thumb: G1 is the right default for most Java apps; tune -XX:MaxGCPauseMillis first before reaching for exotic flags.

ZGC (-XX:+UseZGC, production-ready since Java 15) is designed for sub-millisecond pause times regardless of heap size (tested up to terabytes). It achieves this with three techniques:

  1. Colored pointers — GC metadata is encoded into the unused upper bits of 64-bit references, eliminating separate per-object GC state.
  2. Load barriers — every reference load (not just writes) triggers a barrier that fixes up pointers during concurrent relocation, so the app never stops while objects are moved.
  3. Concurrent relocation — objects are moved while the application runs, unlike G1 which stops for evacuation.
java -XX:+UseZGC -Xmx16g MyLatencySensitiveApp

Choose ZGC when tail latency (p99/p999) matters more than throughput — e.g., real-time APIs, trading systems, or interactive services.

Rule of thumb: ZGC trades some throughput for near-zero pause times; G1 is simpler and usually sufficient unless you're measuring tail latencies below 10 ms.

Shenandoah is a low-pause GC developed by Red Hat, available in OpenJDK since Java 12. Like ZGC it relocates objects concurrently, but uses Brooks pointers (an indirection word in the object header) rather than colored pointer bits, making it compatible with 32-bit builds and compressed oops more easily.

java -XX:+UseShenandoahGC MyApp
Aspect ZGC Shenandoah
Pause target < 1 ms < 10 ms
Concurrent relocation yes yes
Mechanism colored pointers + load barriers Brooks pointer + barriers
Vendor Oracle/OpenJDK Red Hat/OpenJDK

Both are good low-pause choices; ZGC has slightly lower pause guarantees but higher CPU overhead from load barriers on every pointer dereference.

Rule of thumb: ZGC or Shenandoah for ultra-low latency; the choice often comes down to benchmark results in your specific workload.

A stop-the-world (STW) pause is a moment when all application threads are suspended so the GC can operate on a consistent snapshot of the heap. Even concurrent collectors like G1, ZGC, and Shenandoah need short STW phases (initial mark, final remark/rerooting) because GC and application threads cannot safely move objects and update every reference atomically without a brief freeze.

Timeline:
  [App threads running] → [STW: initial mark ~few ms] → [App + GC concurrent]
                       → [STW: remark ~few ms] → [App threads running]

The goal of modern collectors is to make STW pauses short and predictable rather than eliminating them entirely (which is impossible in current JVM designs without hardware transactional memory).

Rule of thumb: no JVM GC is truly "pause-free"; the question is how long and frequent the pauses are.

GC roots are objects that are guaranteed to be live — they are the seeds from which the collector traces all reachable objects. Any object not reachable from a GC root is dead and eligible for collection.

Common GC roots:

  • Local variables in active stack frames
  • Static fields of loaded classes
  • JNI references (held by native code)
  • References in active threads
static Map<Integer, Object> registry = new HashMap<>();
// Anything stored in registry is reachable via a static field (GC root)
// and will NEVER be collected while the class is loaded

Rule of thumb: a Java memory leak is almost always an object still reachable from a GC root that you forgot to remove (e.g., an event listener, a static cache entry, a ThreadLocal that was never cleared).

If a class overrides Object.finalize(), the GC places unreachable instances of that class on a finalization queue instead of immediately reclaiming them. A dedicated finalizer thread eventually calls finalize(), then the object becomes reclaimable on the next GC cycle.

Problems with finalization:

  • Unpredictable timing — you don't know when or if finalize() will run.
  • Performance — objects survive at least two extra GC cycles.
  • Resurrectionfinalize() can store this somewhere, making the object live again — a source of hard-to-find bugs.
  • Deprecated in Java 9, scheduled for removal.
// Preferred alternative: AutoCloseable + try-with-resources
class Resource implements AutoCloseable {
    @Override public void close() { /* release resource deterministically */ }
}
try (Resource r = new Resource()) { r.use(); }

Rule of thumb: never rely on finalize(); use AutoCloseable and try-with-resources for deterministic resource cleanup.

Java 9+ uses the Unified Logging framework for GC output:

java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m MyApp

A typical G1 Minor GC log line:

[2.345s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 128M->64M(512M) 12.345ms

Key fields: pause type, heap before → after (heap capacity), pause duration.

Useful selectors:

  • gc — one line per GC event
  • gc* — verbose, includes region details
  • gc+heap=debug — heap breakdown before/after

Rule of thumb: always enable GC logging in production with rolling files; you cannot replay GC history if you don't capture it, and it is the first thing you need when diagnosing a latency incident.

Flag Effect
-XX:+UseG1GC Select G1 (default Java 9+)
-XX:MaxGCPauseMillis=N G1 pause target (soft goal, default 200 ms)
-XX:GCTimeRatio=N Ratio of GC time to app time (Parallel GC)
-XX:NewRatio=N Old:Young size ratio (e.g., 2 = 1/3 Young)
-XX:SurvivorRatio=N Eden:Survivor ratio within Young Gen
-XX:MaxTenuringThreshold=N GC cycles before promotion to Old Gen
-XX:G1HeapRegionSize=N G1 region size (1–32 MB, power of 2)
-XX:G1NewSizePercent / -XX:G1MaxNewSizePercent Young Gen size range for G1
# Common production starting point for G1:
java -XX:+UseG1GC -Xms4g -Xmx4g -XX:MaxGCPauseMillis=100 \
     -XX:+HeapDumpOnOutOfMemoryError MyApp

Rule of thumb: measure first with GC logs before tuning; changing flags without data is guesswork.

Each time an object survives a Minor GC it is copied to the other Survivor space and its age counter (stored in the mark word, max 15 bits) is incremented. When the age reaches -XX:MaxTenuringThreshold (default 15), the object is promoted to Old Gen on the next Minor GC.

Premature promotion also occurs when:

  • A Survivor space is more than 50 % full during a Minor GC — the JVM lowers the effective threshold dynamically (adaptive tenuring).
  • The object is too large for Survivor — it bypasses Young Gen entirely and goes straight to Old Gen (or a humongous region in G1).
// Anti-pattern: large, long-lived cache that pollutes Old Gen quickly
static Map<String, byte[]> cache = new ConcurrentHashMap<>();
void put(String key) {
    cache.put(key, new byte[1024 * 1024]); // immediately humongous in G1
}

Rule of thumb: if Old Gen grows steadily without a leak, your objects are living too long — reduce object scope or use a proper cache with eviction.

Java provides reference wrappers that allow the GC to collect the referent under different urgency levels, enabling memory-sensitive caches:

Type GC collects when Main use
StrongReference (normal) Never while reachable Everything
SoftReference<T> Before throwing OOM Memory-sensitive caches
WeakReference<T> Next GC, regardless of memory Canonicalizing maps
PhantomReference<T> After finalization GC notification / cleanup
Cache<K, V> cache = new ConcurrentHashMap<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = ref.get(); // null if GC evicted it

WeakHashMap uses weak references as keys — entries disappear as soon as the key has no strong referent elsewhere.

Rule of thumb: prefer explicit cache eviction (size/time bounds) over SoftReference caches — the GC's eviction timing is unpredictable.

A concurrent mode failure (also called evacuation failure in G1) occurs when the GC cannot complete its concurrent collection fast enough — Old Gen fills up while the concurrent marking cycle is still running. The JVM falls back to a full stop-the-world collection to avoid an OOM.

In G1 this appears in logs as:

[gc] GC(42) To-space exhausted
[gc] GC(43) Pause Full (G1 Compaction Pause) 2048M->1200M(2048M) 8432.345ms

Common causes and fixes:

Cause Fix
Objects promoted to Old Gen faster than G1 marks them Increase heap (-Xmx) or reduce -XX:MaxGCPauseMillis to trigger collection sooner
Too few G1 concurrent threads Increase -XX:ConcGCThreads
App creating humongous objects frequently Increase -XX:G1HeapRegionSize

Rule of thumb: repeated evacuation failures are a sign that the heap or GC concurrency settings are undersized for the allocation rate.

Throughput = fraction of CPU time spent executing application code (not GC). Measured over long windows (minutes); long individual pauses are acceptable.

Latency = individual GC pause duration. Matters for interactive apps, APIs with SLAs, or real-time systems where a 500 ms pause is unacceptable.

GC Optimises Best for
Parallel GC Throughput Batch jobs, offline processing
G1 Balanced (default 200 ms goal) General-purpose servers
ZGC / Shenandoah Latency (< 1–10 ms) Low-latency APIs, trading, streaming
# Batch job: maximise throughput
java -XX:+UseParallelGC -XX:GCTimeRatio=19 BatchJob   # 95 % app, 5 % GC

# API server: limit pause time
java -XX:+UseG1GC -XX:MaxGCPauseMillis=50 ApiServer

Rule of thumb: measure your SLA requirements first — if p99 latency already meets the SLA with G1, there is no reason to switch to ZGC.

System.gc() is a hint to the JVM to run a full GC. The JVM may ignore it (especially with -XX:+DisableExplicitGC, which is common in production). When it is honoured it triggers a Full GC — the most expensive GC event.

System.gc(); // hint — may trigger Full GC, may be ignored

Why to avoid it:

  • Forces an expensive Full GC at an unpredictable point, causing a long pause that can violate SLAs.
  • The GC is better at deciding when to collect than application code.
  • Commonly a sign of premature optimisation or misunderstanding of GC.

Legitimate use: in benchmarking code, called before each measurement to start from a clean heap. Even then, use Runtime.getRuntime().gc() in a loop and confirm GC actually ran with logs.

Rule of thumb: never call System.gc() in production code; remove it immediately if you find it in a code review.

A safepoint is a point in the execution of application threads at which the JVM can safely inspect or modify the heap — all threads are at a consistent, known state (no partially-constructed objects being referenced). Before a stop-the-world GC phase, the JVM requests all threads to reach their next safepoint and halt.

Safepoint checks are inserted by the JIT at method returns, backward branches (loop backs), and certain other locations. A thread that is executing a long counted loop without a backward branch may delay GC by failing to reach a safepoint quickly — the "safepoint bias" problem, mitigated in Java 10+ with -XX:+UseCountedLoopSafepoints.

// A tight counted loop may delay safepoint:
for (int i = 0; i < 1_000_000_000; i++) {
    sum += array[i]; // no backward branch safepoint in old JVMs
}

Rule of thumb: if GC pause logs show suspiciously long "time to safepoint" entries, look for long-running counted loops in the hot path.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel