TL;DR ⚡

5-second recall

  • GC decides garbage by reachability, NOT recency. Trace from GC roots; unreachable = garbage.
  • Generational hypothesis: most objects die young → young gen collected often & cheap; survivors promoted to old gen (collected rarely).
  • Phases: Mark → Sweep → Compact. Compaction removes fragmentation → fast bump-pointer allocation.
  • Modern collectors (G1, ZGC, Shenandoah) minimize stop-the-world pauses on big heaps by working concurrently and in small regions.

1. How GC decides what’s garbage — REACHABILITY (not recency)

GC does not track when an object was last used (that’s an LRU cache, a different thing). It tracks whether an object is still reachable.

  • Start from GC roots and trace every reference. Reachable = live. Unreachable = garbage.
  • An object used 1ms ago with no references → garbage. An object untouched for an hour but still referenced → live.
flowchart TD
    R["GC Roots<br/>(stack locals, statics, threads, JNI)"] --> A["Object A"]
    A --> B["Object B"]
    R --> C["Object C"]
    D["Object D<br/>(unreferenced)"]
    E["Object E"] --> D
    D -.- G(("GARBAGE:<br/>D & E<br/>unreachable"))
    classDef live fill:#9FE1CB,stroke:#0F6E56,color:#f44336;
    classDef dead fill:#F7C1C1,stroke:#A32D2D,color:#00008B;
    class R,A,B,C live;
    class D,E dead;

D and E reference each other, but nothing reachable from the roots points to them → both are garbage. (Reachability handles cycles that simple reference-counting can’t.)

What are GC roots?

References the JVM treats as “definitely alive, trace from here.” Categories are fixed:

  • Local variables on every thread’s stack (not one thread — all of them).
  • Static fields (alive while the class is loaded).
  • Active threads.
  • JNI / native references.

2. Why split the heap? — the Weak Generational Hypothesis

“Most objects die young.” Every request creates piles of short-lived temporaries (DTOs, StringBuilders, intermediate lists) that are garbage the instant the method returns. Few objects (caches, singletons, sessions) live long.

Design that exploits this:

  • Young generation — new objects; collected frequently but cheaply (each pass reclaims most of it, only copies the few survivors).
  • Old (tenured) generation — the rare survivors; collected infrequently (expensive to scan).

Heap layout (diagram)

flowchart LR
    subgraph Heap
      subgraph Young["Young gen (minor GC, frequent/cheap)"]
        Eden["Eden<br/>new objects"] --> S0["S0"]
        S0 --> S1["S1"]
      end
      Old["Old / Tenured gen<br/>(major GC, rare)"]
    end
    S1 -- "survived enough GCs → promote" --> Old

Young gen internals

  • Split into Eden + two Survivor spaces (S0, S1).
  • New objects → Eden. Minor GC copies survivors Eden→Survivor, and between survivors each cycle.
  • Each object has an age = number of minor GCs survived. Past the tenuring threshold (max 15) → promoted to old gen.
  • Promotion is by survival count, NOT by “least recently used.”

3. GC types

TypeCollectsCost
Minor GCyoung gen onlyfrequent, fast
Major GCold genslower
Full GCyoung + old + metaspacemost expensive, longest pause

Trap: “major” vs “full” are used loosely. Safe phrasing: minor collects young; full collects the whole heap. Knowing full = young+old+metaspace is the senior signal.

When does a major/full GC run?

No fixed number of minor cycles. It’s triggered by pressure:

  • Old gen fills past a threshold (promotions kept filling it).
  • Allocation needs space that isn’t available.
  • Metaspace fills, or System.gc() is called.

Answer: “driven by occupancy/pressure, not a fixed cadence.”


4. The algorithm — Mark → Sweep → Compact

  1. Mark — trace from roots, flag all reachable (live) objects.
  2. Sweep — reclaim the unmarked (garbage).
  3. Compact — slide survivors together into one contiguous block.
Start:    [L][x][L][L][x][L]      L = live, x = garbage
Mark:     [L][x][L][L][x][L]      live ones flagged
Sweep:    [L][ ][L][L][ ][L]      garbage freed → scattered gaps (fragmented)
Compact:  [L][L][L][L][____ ]     survivors slid together → one big free block

Fragmentation (the scattered gaps after sweep) is why compaction matters: afterward, allocation is a fast bump pointer — just grab the next slot in the contiguous free block.

Why compaction (removing fragmentation) helps

Fragmentation = free space scattered in small gaps between live objects.

  • You may have enough total free space but no contiguous block big enough → allocation fails / forces GC.
  • Compaction slides survivors to one end → one big free block → allocation becomes a fast bump pointer (“grab the next slot”), no hunting.

5. Stop-the-world & why modern collectors exist

Stop-the-world (STW) = GC freezes all application threads while it works.

The problem: on a big heap (say 32GB old gen), a single STW mark-sweep-compact pause can be hundreds of ms to seconds — unacceptable for low-latency systems (trading, real-time). Pause time grows with heap size.

The fix modern collectors use: do most work concurrently (while app runs) and in small chunks instead of one giant pause.

Collectors spectrum (diagram)

flowchart LR
    S["Serial<br/>1 thread, STW"] --> P["Parallel<br/>N threads, STW"]
    P --> G["G1<br/>heap regions, balanced,<br/>pause target"]
    G --> Z["ZGC / Shenandoah<br/>concurrent, sub-ms pauses"]
    classDef tp fill:#FAC775,stroke:#854F0B,color:#412402;
    classDef bal fill:#B5D4F4,stroke:#0C447C,color:#042C53;
    classDef low fill:#9FE1CB,stroke:#0F6E56,color:#04342C;
    class S,P tp;
    class G bal;
    class Z low;

Left = throughput / longer pauses → right = low latency / tiny pauses. G1 is the default since JDK 9.

G1 & “chunks of the heap” = REGIONS

The JDK 7u4/JDK 8-era improvement you’re remembering: G1 (Garbage-First) divides the heap into many equal-sized regions (1–32MB each) instead of contiguous young/old blocks.

  • A region is the unit of allocation and reclamation; each is dynamically tagged Eden / Survivor / Old (or “humongous” for big objects).
  • G1 collects the regions with the most garbage first (→ the name) and can hit a pause-time target (-XX:MaxGCPauseMillis).
  • Introduced JDK 7u4 (for heaps > 4GB), matured in JDK 8, default since JDK 9 (JEP 248).
CollectorIdeaUse when
Serialone thread, STWtiny heaps, single-CPU
Parallel (Throughput)multiple threads, STWbatch jobs; max throughput, pauses OK
G1 (Garbage First)heap split into many regions; collects the regions with most garbage first; mostly concurrent; predictable pause target (-XX:MaxGCPauseMillis)default since Java 9; balanced latency/throughput
ZGC / Shenandoahconcurrent compaction; pauses < a few ms even on multi-TB heapslow-latency (trading, real-time)

G1 in one line

Splits the heap into equal regions (not fixed young/old blocks), collects the fullest-of-garbage regions first, works mostly concurrently, and lets you set a pause-time goal — so no single huge STW pause.

ZGC vs Shenandoah — same goal, different mechanism

Both are concurrent, region-based, sub-ms-pause collectors — but they are different collectors (and different vendors), not one thing:

ZGC (Oracle)Shenandoah (Red Hat)
Concurrent relocation viacolored pointers — metadata bits in the 64-bit reference + load barriers; forwarding tableBrooks forwarding pointers — an extra pointer in each object header pointing to its current location
Pointer width64-bit only (needs the spare bits)works on 32-bit & 64-bit
IntroducedJDK 11 (prod JDK 15, generational JDK 21)JDK 12 (backported to 8/11; generational prod JDK 25)
In Oracle JDK?yesno (Oracle ships ZGC instead)

Both reach the same outcome: move objects while the app runs, so pauses stay < ~1ms regardless of heap size. The difference is where the “has this object moved?” info lives — in the pointer (ZGC) vs in the object header (Shenandoah).

Do the expensive work concurrently

ZGC and Shenandoah both do marking and compaction concurrently with the application, so STW pauses stay in the sub-millisecond range regardless of heap size — the answer for latency-critical apps.

G1 vs ZGC (common interview compare)

G1 (Garbage-First)ZGC
Regionsyesyes
Markingpartly concurrentconcurrent
Compaction / moving objectsstop-the-worldconcurrent (colored pointers + load barriers)
Pause timetens–hundreds of ms (grows with heap)< ~1ms, independent of heap size
Best forbalanced general workloadslow latency / very large heaps (TB)
Default?yes, since JDK 9opt-in (-XX:+UseZGC)

One-liner: “G1 is throughput-oriented with bounded pauses, but its compaction is still STW so pauses grow with heap size; ZGC compacts concurrently, so pauses stay sub-ms even on huge heaps."

"Is GC even needed with huge RAM (512GB)?” — flip the assumption

Two things get conflated here:

  1. Metaspace ≠ heap. Heap holds objects (capped by -Xmx); Metaspace holds class metadata in native memory. Metaspace growing doesn’t make heap = RAM — they’re independent.
  2. GC isn’t about running out of RAM — it’s about reclaiming unreachable objects. Without GC, even 512GB fills with garbage and crashes; more RAM only delays it.

Senior punchline: a big heap makes GC more critical, not less — a stop-the-world scan of 512GB could freeze the app for seconds. That’s exactly why ZGC/Shenandoah exist. Large heaps don’t remove the need for GC; they demand low-pause concurrent collectors.


Collectors by JDK version (timeline)

CollectorIntroducedStatus notesBy
SerialJDK 1.3small heaps / single CPUOracle
Parallel (throughput)JDK 1.4default in JDK 5–8Oracle
CMSJDK 1.4deprecated JDK 9, removed JDK 14Oracle
G1experimental JDK 7u4default since JDK 9Oracle
ZGCexperimental JDK 11production JDK 15, generational JDK 21Oracle
Shenandoahupstream JDK 12backported to JDK 8/11; not in Oracle JDKRed Hat
Epsilon (no-op)JDK 11does nothing — testing/benchmarksOracle

Shenandoah’s odd name (a Virginia valley/river + folk song) is a tell that it’s a Red Hat collector, not an Oracle one — which is why Oracle JDK ships ZGC instead.

6. Tuning flags worth knowing

  • -Xms / -Xmx — initial / max heap.
  • -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseParallelGC — pick collector.
  • -XX:MaxGCPauseMillis=200 — G1 pause-time goal.
  • -XX:MaxTenuringThreshold — promotion age (≤15).
  • -XX:MaxMetaspaceSize — cap metaspace (Java 8+; PermGen is gone).

References

Official JDK / Oracle GC documentation (authoritative)

Official JEPs (the “why” behind each change)

Learning / walkthroughs

Test Yourself

How does GC decide what’s garbage?::Reachability — trace from GC roots (all threads’ stack locals, statics, active threads, JNI); unreachable objects are garbage. Not recency.
What is the weak generational hypothesis?::Most objects die young — so young gen is collected frequently and cheaply, and rare survivors are promoted to the old gen (collected rarely).
When is an object promoted to old gen?::When its age (minor GCs survived) passes the tenuring threshold (max 15) — by survival count, not recency.
Minor vs major vs full GC?::Minor = young only (fast/frequent); major = old gen; full = whole heap + metaspace (most expensive).
After how many minor GCs does a full GC run?::No fixed number — triggered by pressure (old gen fills, allocation needs space, metaspace fills, System.gc()).
Why compact the heap?::Fragmentation leaves scattered gaps with no contiguous block for new objects; compaction slides survivors together → fast bump-pointer allocation.
What is stop-the-world and why do G1/ZGC exist?::STW freezes all app threads during GC; on big heaps one pause can be seconds. G1 (regions, concurrent, pause target) and ZGC/Shenandoah (concurrent compaction, sub-ms pauses) minimize it for low-latency apps.
G1 in one sentence?::Splits heap into regions, collects the ones with most garbage first, works mostly concurrently, and targets a configurable max pause time.
What are G1’s ‘chunks of the heap’ called, and when did it arrive?::Regions (equal-sized, 1–32MB). G1 introduced JDK 7u4 for large heaps, matured in JDK 8, default since JDK 9.
G1 vs ZGC — key difference?::Both region-based, but G1’s compaction is stop-the-world (pauses grow with heap), while ZGC compacts concurrently (sub-ms pauses regardless of heap size).
Is GC still needed with huge RAM like 512GB?::Yes, more than ever — GC reclaims unreachable objects (RAM size doesn’t remove that need), and a STW scan of a huge heap would freeze the app for seconds, which is why concurrent collectors like ZGC exist.
Which JDK introduced ZGC and Shenandoah?::ZGC: experimental JDK 11, production JDK 15, generational JDK 21 (Oracle). Shenandoah: upstream JDK 12, backported to 11 (Red Hat; not in Oracle JDK).
Why was Metaspace introduced over PermGen?::PermGen was fixed-size and threw OutOfMemoryError on class-heavy apps (redeploys, dynamic proxies). Metaspace moved class metadata to auto-growing native memory — killing those OOMs and simplifying heap GC. It doesn’t grow the heap.
ZGC vs Shenandoah — how do they differ?::Both concurrent, sub-ms, region-based. ZGC uses colored pointers (metadata bits in the 64-bit reference) + load barriers; Shenandoah uses Brooks forwarding pointers in each object header. ZGC = Oracle (64-bit only); Shenandoah = Red Hat (also 32-bit).