← Back to Design & Development
JVM · Interview Prep

Java Garbage Collector — The Interview Guide

What GC actually does, how the heap is split, and how G1, ZGC & Shenandoah work — explained the way you'd say it in an interview, not the way a JVM textbook would.

01

What GC Does — In 30 Seconds

In C, you allocate with malloc and free with free. Forget to free → memory leak. Free twice → crash. Java said: "let the runtime do it." That's the GC.

The one-line definition

An object is garbage if no live thread can reach it through any chain of references. The GC's job is simple: find unreachable objects, free their memory.

Think of the GC as the building's security guard. It walks from the official entrances (called GC Roots — thread stacks, static fields) and checks every room it can reach. Anyone in a room nobody walked into? Out they go.

What are GC Roots?

The starting points of the reachability walk. If an object is reachable from a root, it's alive:

  • Local variables on every running thread's stack
  • Static fields of loaded classes
  • Active monitors (objects locked by synchronized)
  • JNI references (native code holding Java objects)
GC = "find what you can reach from the roots, delete the rest." Everything else (generations, regions, ZGC's tricks) is just how to do that fast without freezing your app.
02

The Heap Layout — Young, Old, Metaspace

The heap is split into regions because most objects die young. This is the famous "weak generational hypothesis" — typically 90%+ of objects become garbage within milliseconds of being created. So the JVM treats young and old objects differently.

The structure

Heap │ ├── Young Gen │ ├── Eden ← every `new` lands here (~80% of young) │ ├── Survivor S0 ← survivors of past Minor GCs │ └── Survivor S1 ← (one of S0/S1 is always empty) │ └── Old Gen (Tenured) ← long-lived objects after surviving enough GCs Metaspace (off-heap) ← class metadata, method bytecode (replaced PermGen in Java 8)
flowchart LR E["Eden"] -->|"survives 1st GC"| S0["Survivor 0"] S0 -->|"survives next GCs"| S1["Survivor 1"] S1 -->|"age threshold reached"| Old["Old Gen"] style E fill:#e8743b,stroke:#e8743b,color:#fff style S0 fill:#58a6ff,stroke:#58a6ff,color:#fff style S1 fill:#58a6ff,stroke:#58a6ff,color:#fff style Old fill:#bc8cff,stroke:#bc8cff,color:#fff

Why three areas in Young Gen?

Two Survivors (S0 + S1) are used for a copying trick. At any moment, one is empty. When GC runs, it copies all live objects from Eden + the full Survivor into the empty Survivor — then wipes the source areas in one shot. Next time, they swap roles. This is why young-gen GC is so fast.

How an object travels

  1. You write new Order() → object lands in Eden.
  2. Eden fills up → Minor GC runs. Live objects copied to a Survivor space, age = 1.
  3. Object survives more GCs → keeps bouncing between S0 ↔ S1, age++ each time.
  4. Age crosses threshold (default 15) → object is promoted to Old Gen.
  5. Old Gen fills up → Major GC runs (much slower).
Eden is a hospital's maternity ward — busy, almost everyone leaves the same day. Survivor is short-stay recovery. Old Gen is long-term care — only the cases that need real care end up there.
Heap is split because young and old objects have different lifecycles. Young → fast copying GC. Old → slower mark-sweep-compact GC. Treating them the same would waste enormous amounts of work.
03

Minor GC vs Major GC vs Full GC

Three terms that get used loosely. Knowing the difference is interview gold.

TypeWhat it cleansHow oftenPauseAlgorithm
Minor GC Young Gen only Frequent (every few seconds) Short (1–10 ms) Copying — copy live objects to empty Survivor, wipe source
Major GC Old Gen only Rare Longer (100 ms+) Mark-Sweep-Compact
Full GC Whole heap (Young + Old + Metaspace) Very rare (something's wrong) Long (seconds possible) Mark-Sweep-Compact, all areas

The Minor GC algorithm — copying

  1. Walk roots, mark every reachable object in Young Gen.
  2. Copy all live objects from Eden + current Survivor → empty Survivor.
  3. Increment each survivor's age. If age > 15 → promote to Old Gen.
  4. Wipe Eden and the old Survivor in O(1) (just reset the pointer).

Cost is proportional to live objects, not heap size. Since 90%+ of Eden is dead, you copy 10% and reclaim 90% essentially for free.

The Major GC algorithm — Mark-Sweep-Compact

  1. Mark — walk roots, mark every reachable object as alive.
  2. Sweep — scan the heap; anything unmarked is freed.
  3. Compact — slide live objects together to eliminate fragmentation (so future allocations have one big contiguous free area).
Minor GC is like cleaning a hotel's checkout floor every hour — fast, only a few rooms matter. Major GC is like deep-cleaning the whole building — rare, slow, but necessary. Full GC is "we're closing the hotel for renovations" — you don't want this in production.
Minor GC = young-only, fast, frequent. Major GC = old-only, slower, rare. Full GC = everything, painful, usually a sign of trouble (heap too small or memory leak).
04

Stop-the-World (STW) — Why GC Pauses Exist

Some GC work needs a consistent snapshot of the heap. To get that, the JVM pauses every application thread. This pause is called Stop-the-World.

Why it's needed

Imagine the GC walks references while your code is busy reassigning them. The GC could miss an object, or copy one that just moved. To avoid this corruption, the JVM halts all application threads at "safepoints" before doing the risky work.

It's like trying to count people in a moving crowd. You can't get an accurate count while everyone's shuffling around. So you ask everyone to freeze for a second, count them, then say "you can move again."

The trade-off every GC makes

  • Old GCs (Serial, Parallel) — pause for the entire collection. Simple but pauses can be seconds.
  • G1 — does most work concurrently with the app. STW pauses are short (~200ms) and bounded by a target you set.
  • ZGC / Shenandoah — almost everything runs concurrently. STW pauses are sub-millisecond, regardless of heap size.
STW = pause where every app thread stops so the GC can work safely. The whole evolution from Serial → Parallel → G1 → ZGC is the story of shrinking STW pauses while keeping correctness.
05

G1 GC — How It Works (Default Since Java 9)

G1 stands for Garbage First. It's the default collector in modern JVMs and what most production servers use. Its key idea: throw out the rigid Eden/Survivor/Old layout and replace it with thousands of small regions.

The big idea — regions

G1 splits the heap into ~2000 equal-sized regions (1–32 MB each). Each region is dynamically labeled as Eden, Survivor, or Old — boundaries shift over time. There's no fixed "young area" — any region can play any role.

flowchart LR subgraph H["G1 Heap — divided into ~2000 regions"] direction LR R1[E]:::e R2[O]:::o R3[O]:::o R4[E]:::e R5[S]:::s R6[O]:::o R7[E]:::e R8[O]:::o R9[E]:::e R10[O]:::o R11[S]:::s R12[F]:::f end classDef e fill:#e8743b,stroke:#e8743b,color:#fff classDef s fill:#58a6ff,stroke:#58a6ff,color:#fff classDef o fill:#bc8cff,stroke:#bc8cff,color:#fff classDef f fill:#1c2230,stroke:#30363d,color:#8b949e

E = Eden · S = Survivor · O = Old · F = Free

Why "Garbage First"?

Each region tracks how much of it is garbage. At collection time, G1 sorts regions by garbage density and collects the most-garbage ones first. Best ROI: clean a 95%-garbage region and you reclaim almost the whole region for the cost of evacuating the 5% that's alive.

Imagine garbage cans on a city street. The cleaning truck has 30 minutes (your pause-time budget). It can't visit every can. So it has data: "this can is 95% full, this one is 20% full." It hits the 95% cans first because that's where the wins are. That's literally G1.

How G1 hits a pause-time target

You tell G1 "I want pauses under 200ms" via -XX:MaxGCPauseMillis=200. G1 keeps stats on how long each region took to evacuate last time. Given the budget, it picks a set of regions that fit in 200ms while reclaiming the most garbage. Whatever doesn't fit is collected next cycle.

The G1 cycle (simple version)

  1. Young collection — evacuates Eden + Survivor regions. STW, but short.
  2. Concurrent marking — when Old Gen is ~45% full, G1 walks the heap concurrently with the app, computing per-region garbage counts.
  3. Mixed collection — next several "young" GCs also evacuate the highest-garbage Old regions. This avoids needing a Full GC.
  4. Full GC — last resort if mixed can't keep up. STW, slow. Bad if it happens often.
If asked "why G1?" — say: "predictable pause times via region selection, no rigid generation boundaries, default since Java 9, sweet spot is 4–32 GB heaps." That's all you need.
G1 = region-based + collect-most-garbage-first + target a pause time. You sacrifice some throughput for predictable pauses, which is what most server apps want.
06

ZGC — Sub-Millisecond Pauses on Huge Heaps

ZGC is built for one job: keep pauses under 1 millisecond, no matter how big the heap is. It can handle 16 TB heaps with the same pause time as an 8 GB heap. The trick: do almost everything concurrently with the app.

A trading firm runs its matching engine on a 256 GB heap. They cannot tolerate a 200ms pause — that's millions of dollars of missed trades. They switch to ZGC. Pauses drop below 1ms regardless of heap size. They sleep again.

The big idea — fully concurrent

Most GCs pause the app for the bulk of their work. ZGC does almost everything while the app keeps running — including the hard part, compaction (moving live objects to defrag the heap). Only tiny coordination steps require STW.

How does it move objects without freezing the app?

This is the magic. ZGC uses two tricks (you only need the high-level idea for an interview):

  • Colored pointers — ZGC stores extra info in unused bits of object references. These bits tell the GC what state the object is in (e.g., "already moved", "needs scanning").
  • Load barriers — every time your code reads a reference, the JVM checks the pointer's color bits. If the object has been moved, the JVM transparently fixes the pointer to the new location. Your code doesn't notice.
Imagine renovating a hotel without closing it. As guests walk down the corridor, a robot whispers "your room moved to floor 7" — and the guest just walks there. No queue, no closure. ZGC's load barrier is that whisper.

What you need to know

  • Pause time: < 1 ms, independent of heap size.
  • Heap range: 8 GB to 16 TB.
  • Trade-off: ~5–15% lower throughput than Parallel GC (because of barrier overhead).
  • Flag: -XX:+UseZGC. Generational variant since JDK 21: -XX:+ZGenerational.
  • Best for: latency-critical services — trading, gaming, real-time APIs.
If asked "what is ZGC?" — say: "concurrent collector with sub-millisecond pauses, scales to terabytes, uses colored pointers and load barriers to move objects while the app runs. Trades throughput for latency. Use it when you can't tolerate >10ms pauses."
ZGC's superpower is pause time independent of heap size. A 16 TB heap pauses no longer than an 8 GB one. That's the breakthrough.
07

Shenandoah — The Other Low-Pause Collector

Shenandoah was built by Red Hat with the same goal as ZGC — short pauses regardless of heap size — but a different mechanism. Both work; ZGC just won the popularity contest.

The big idea — concurrent compaction with a forwarding pointer

Like ZGC, Shenandoah does compaction concurrently with the app. Instead of colored pointers, it uses a simpler trick called the Brooks pointer: every object has an extra header field that points to itself by default. When the object is being moved, that pointer redirects to the new location.

Object header: [class ptr][flags][Brooks fwd ptr]──► self (or new location) ▲ Every reference read goes through this pointer.

How it works in plain English

  1. Shenandoah marks live objects concurrently (just like G1's concurrent mark).
  2. It picks high-garbage regions to evacuate.
  3. While the app is running, it copies live objects from those regions to fresh regions.
  4. If the app reads an object that's been copied, the Brooks pointer redirects the read to the new location — transparently.
  5. Old regions are freed once everything's evacuated.
It's like adding a "temporary mail forwarding" sticker to every mailbox. When you move, you don't tell every sender — you just leave the sticker, and mail finds its way. Shenandoah is the JVM doing this for object references.

What you need to know

  • Pause time: < 10 ms (close to ZGC, slightly higher).
  • Heap range: 4 GB to ~1 TB.
  • Trade-off: indirection on every read (Brooks pointer dereference) → similar throughput cost to ZGC.
  • Flag: -XX:+UseShenandoahGC. Available in OpenJDK builds (Red Hat-led).
  • Available even on smaller / 32-bit JVMs (ZGC needs 64-bit + larger heaps).
If asked "Shenandoah vs ZGC?" — say: "both target sub-10ms pauses concurrently. ZGC uses colored pointers (needs 64-bit, 4GB+), scales bigger (up to 16 TB). Shenandoah uses Brooks forwarding pointers, works on smaller heaps too. Choose based on heap size and JDK build."
Shenandoah and ZGC are siblings — both keep the app running during compaction. Different mechanisms, similar results. Pick based on your heap size and which is supported in your JDK.
08

Side-by-Side — Which GC When?

The cheat-sheet you should be able to recite in your sleep.

GC Pause Throughput Heap Size Best For Flag
Serial Long (single-threaded) Low < 100 MB CLI tools, embedded -XX:+UseSerialGC
Parallel Long (multi-threaded STW) Highest 1–8 GB Batch jobs (throughput > latency) -XX:+UseParallelGC
G1 (default) Predictable (~200ms) Medium-High 4–32 GB Most server apps -XX:+UseG1GC
ZGC < 1 ms Medium 8 GB – 16 TB Latency-critical, huge heaps -XX:+UseZGC
Shenandoah < 10 ms Medium 4 GB – 1 TB Low-pause on smaller heaps -XX:+UseShenandoahGC

The decision tree

Is throughput the only thing that matters (batch job)? → Parallel GC Need predictable ~200ms pauses on 4–32 GB heap? → G1 (the default — start here) Need pauses < 10ms? Or huge heap (> 32 GB)? → ZGC (preferred) or Shenandoah Tiny app or embedded? → Serial GC
When the interviewer asks "which GC would you pick?" — always start with "depends on the workload." Then run the decision tree above. Showing you think about the SLA before picking the tool is the signal they want.
No "best" GC — only the best fit for your workload. Default to G1 unless you have a specific reason (huge heap → ZGC, batch job → Parallel, embedded → Serial).
09

Reference Types — Strong, Soft, Weak, Phantom

99% of references in Java are "strong" — the GC will never collect them while they exist. But sometimes you want weaker semantics. Java has three flavors for that.

TypeGC behaviorUse case
Strong (default) Object never collected while a strong ref exists Normal fields, locals, parameters
Soft (SoftReference) Collected only when JVM is low on memory Memory-sensitive caches
Weak (WeakReference) Collected at next GC if no strong refs WeakHashMap, listener registries
Phantom (PhantomReference) Object already finalized; reference enqueued for cleanup Resource cleanup (replaces finalize())

Most-asked interview example — WeakHashMap:

Map<Component, Metadata> meta = new WeakHashMap<>();
meta.put(component, metadata);
// When `component` is GC'd from elsewhere, the map entry vanishes automatically.
// No need to manually `.remove()` — perfect for caches keyed by lifetime.
finalize() is deprecated and unreliable — unpredictable timing, can resurrect objects, blocks the finalizer thread. Use java.lang.ref.Cleaner (Java 9+) or try-with-resources (AutoCloseable) instead.
Strong refs for everything, weak refs for "auto-clean caches", phantom refs for native resource cleanup. That's 99% of what you need.
10

Memory Leaks in Java — Yes, They Exist

"Java has GC, so it can't leak." Wrong. You can't forget to free, but you can forget to unreference — and the symptom is the same: heap fills up, OOM crash.

The 5 most common leak patterns

1. Unbounded static collections

A static HashMap used as a cache, no eviction. Every entry stays alive forever.

Fix: Caffeine or Guava cache with size + TTL limits.

2. Unremoved listeners

eventBus.subscribe(this) → no unsubscribe → bus holds a reference forever.

Fix: always pair subscribe/unsubscribe.

3. ThreadLocals in pooled threads

Servlet thread pools reuse threads. ThreadLocal set in request 1 persists into request 2.

Fix: always tl.remove() in finally.

4. Inner classes capturing outer

Non-static inner / anonymous class / lambda capturing this. If it outlives the outer (e.g., put in an executor), the outer can't be GC'd.

Fix: use static nested classes when possible.

5. Unclosed resources

Streams, connections, native buffers (DirectByteBuffer) not closed. Heap looks fine; native memory grows.

Fix: try-with-resources for everything AutoCloseable.

How to debug a leak — 4 steps

  1. Confirm it's a leak. Plot heap-after-Full-GC over time. Trending up = leak.
  2. Take a heap dump: jmap -dump:live,format=b,file=heap.hprof <pid>
  3. Open in Eclipse MAT → run "Leak Suspects" — points at top retainers.
  4. "Path to GC roots" in MAT shows the chain keeping the leaked object alive. Fix that chain.
"How would you debug a Java memory leak?" → recite the 4 steps above. Naming Eclipse MAT and "Path to GC roots" signals real production experience.
Java leaks are reachability bugs, not memory-management bugs. Every leak boils down to: "this object is still reachable from a root I forgot to clear."
11

Top Interview Q&A — The 60-Second Answers

The exact answers to give. Lead with the punchline, expand only if asked.

Explain how garbage collection works in Java.

4-beat structure: (1) An object is garbage if no live thread can reach it from any GC root. (2) The heap is split into Young + Old based on the "most objects die young" hypothesis. (3) Young uses a fast copying collector — Eden + two Survivors, objects bounce between Survivors until old enough to promote. (4) Old uses Mark-Sweep-Compact, slower but rarer. Default collector since Java 9 is G1; latency-critical apps use ZGC.

What's Stop-the-World?

A pause where every application thread halts so the GC can do work that needs a consistent heap snapshot. Length depends on heap size and collector. Old GCs (Parallel) had STW for the whole collection. G1 minimizes STW by doing most work concurrently. ZGC keeps STW under 1ms regardless of heap size.

Difference between Minor GC and Full GC?

Minor GC = Young Gen only, copying algorithm, fast (1–10ms), frequent. Full GC = whole heap (Young + Old), Mark-Sweep-Compact, slow (100ms to seconds), rare. Frequent Full GCs in production usually mean either heap is too small or there's a memory leak.

What is G1? Why is it the default?

G1 ("Garbage First") splits the heap into ~2000 small regions. It tracks how much garbage each region has and collects the most-garbage ones first. You set a target pause time (e.g., 200ms) and G1 picks how many regions fit in that budget. Default since Java 9 because it gives predictable pauses on multi-GB heaps without much tuning.

What's ZGC and when would you use it?

Concurrent collector that keeps pauses under 1ms regardless of heap size — works from 8 GB up to 16 TB. Uses "colored pointers" and "load barriers" to relocate objects while the app keeps running. Costs ~10% throughput. Use it when you can't tolerate >10ms pauses — trading systems, real-time APIs, games.

ZGC vs Shenandoah?

Both target sub-10ms concurrent collection. ZGC uses colored pointers (needs 64-bit, 4GB+ heaps), scales bigger, ships in mainline OpenJDK. Shenandoah uses Brooks forwarding pointers, works on smaller / older JVMs, was developed by Red Hat. Pick based on your JDK build and heap size.

What are GC Roots?

The starting points for the GC's reachability walk: (1) local variables on every active thread's stack, (2) static fields of loaded classes, (3) JNI references from native code, (4) objects held by active monitors (synchronized blocks). An object is alive if any chain of references reaches it from a root.

Can you have a memory leak in Java?

Yes — they're "reachability leaks", not allocation leaks. Top patterns: unbounded static caches, unremoved listeners, ThreadLocals in pooled threads, inner-class capture, unclosed native resources. Find them with jmap heap dumps + Eclipse MAT's "path to GC roots".

How would you tune GC?

"I don't, until I have to." Then: (1) pick the right collector for the workload, (2) size the heap (-Xms = -Xmx in containers), (3) turn on GC logs (-Xlog:gc*). If pauses are bad, profile to find allocation hotspots. The single biggest lever is allocation rate — reducing it usually helps more than any flag.

Difference between PermGen and Metaspace?

PermGen (pre-Java 8) was inside the heap, fixed-size, held class metadata. Filled up easily — classic OutOfMemoryError: PermGen space in Spring apps. Metaspace (Java 8+) lives in native memory, grows automatically (cap with -XX:MaxMetaspaceSize), pairs better with class unloading.

Memorize 5 of these word-for-word. In interviews, fluent answers to GC questions move you from "studied Java" to "shipped Java in production."
GC questions test three things at once: systems thinking, JVM knowledge, and trade-off reasoning. Master the 11 answers above and you're set for any GC question that comes up.

Did this make GC click? If you walked away knowing what to say in interviews — tap the ❤️.