ZGC compacts the heap by moving live objects while application threads keep reading and writing them. Describe how it chooses what to move, how it prevents threads from working on a stale copy, and at what point the old memory can be handed back for reuse.
answer
- Relocation set = sparsest pages, most bytes freed per byte copied
- Off-heap forwarding table: old address → new address
- Mutator may copy the object itself, CAS decides the winner
- Pages freed immediately after copying, not after remapping
- Remap rides along with the NEXT cycle's marking
basics
~20 sZGC selects the pages holding the most garbage as the relocation set, then copies their live objects concurrently. Off-heap forwarding tables map old to new addresses, and load barriers redirect — or perform — each move via compare-and-set. Pages are freed immediately; stale references get remapped lazily.
solid answer
~50 sAfter marking, ZGC knows how much live data each page (ZPage) holds and picks the **relocation set**: the sparsest pages, where evacuating a few survivors frees the most memory. GC workers then copy those live objects into fresh pages while the application runs. For each relocated page, an **off-heap forwarding table** maps old address → new address. Correctness comes from the load barrier: any thread that loads a reference pointing into the relocation set is diverted into the table. If the object has not been copied yet, that thread copies it and installs the entry with a compare-and-set, so exactly one copy wins. Because a stale reference is detectable from its color alone — no need to inspect the old object — **the old pages are freed as soon as their live objects are copied**, not when the last stale reference is fixed. Remaining stale references are repaired lazily by barriers and, wholesale, by the next cycle's marking traversal, which walks the graph and remaps as it goes.
go deeper
Know that ZGC copies live objects out of mostly-empty regions while the program runs, and that a barrier keeps threads from using the old location.
Explain relocation-set selection by garbage ratio, forwarding tables, and that pages are freed as soon as their survivors are copied.
Cover the CAS-based race resolution, mutator-assisted copying, why state-in-the-pointer permits immediate page reclamation, and the headroom requirement.
Discuss the capacity implications: compaction cost scales with live data not heap size, fragmentation stays bounded, and the collector must always win the race against allocation or degrade into stalls.
## Why concurrent relocation is the hard part Marking concurrently is comparatively easy: the object graph changes under you, and barriers plus a snapshot discipline handle it. **Moving** objects concurrently is much harder, because a thread can be holding a reference to an object that no longer lives where the reference says. Get this wrong and the application reads freed memory or two threads mutate two different copies of the same object. ZGC solves it with three ingredients: region-based heap layout, forwarding tables, and the load barrier. ## The heap layout ZGC's heap is divided into **ZPages** of a few sizes: *small* pages (2 MB, holding objects up to a few hundred KB), *medium* pages (32 MB, for larger objects), and *large* pages, each holding a single huge object. Large pages are never relocated — copying a multi-megabyte object to defragment is not worth it — so compaction concerns small and medium pages. Regions matter because compaction becomes *evacuation*: instead of sliding everything down as a classic compactor would, ZGC copies the survivors out of a chosen subset of pages into fresh pages, and frees the source pages whole. ## Choosing the relocation set Concurrent marking leaves per-page liveness statistics. Selecting the relocation set is then an optimization: pick the pages with the *lowest live-data ratio*, because each byte copied there frees the most bytes. A page that is 5 % live gives back 95 % of its space for the cost of copying 5 %. Fully empty pages are simply freed with no copying at all. This selection runs concurrently and ends with a short `Pause Relocate Start` that flips the remap color and fixes the roots so thread-held references point into the new world. ## Forwarding tables For every page in the relocation set, ZGC allocates an **off-heap forwarding table** — a small hash table keyed by the object's offset in the old page, whose value is the object's new address. Off-heap is deliberate: the forwarding data must survive the freeing of the old page, and it must not be reachable through a colored pointer. This is a design divergence worth noting: some concurrent compactors keep an indirection word in the object header, which means the old copy must stay alive until every reference has been updated. ZGC's state-in-the-pointer scheme means a mutator can tell from the *reference alone* that it must consult the collector, so the old copy is not needed as a signpost. ## The relocation protocol While the concurrent relocate phase runs: - GC workers walk the relocation set, copy each live object to a new page, and publish the mapping with a compare-and-set into the forwarding table. - Any application thread that loads a reference into a relocation-set page hits the load barrier's slow path. It looks in the forwarding table. If a mapping is present, it uses the new address. If not, it **copies the object itself**, then CASes the mapping in. If its CAS loses — a GC worker or another mutator got there first — it abandons its copy and uses the winner's address. Either way there is exactly one authoritative copy. - The barrier then self-heals: the corrected pointer is written back into the field it came from. Races are handled by that single CAS. There is never a window where two threads believe different addresses are current, because the CAS on the forwarding entry is the linearization point. ## When memory comes back As soon as a page's live objects have all been copied, the page is **freed and available for allocation** — even though references elsewhere in the heap still point into it. Those references are *bad-colored*, so anyone loading one is intercepted before dereferencing. This is what makes ZGC's memory reclamation prompt: free space returns during the relocate phase, not after a global fix-up. The forwarding tables must be retained until every reference into their pages has been repaired. That happens two ways: opportunistically, by load barriers as the application touches fields, and systematically, during the **next cycle's concurrent marking**, which traverses the reachable graph anyway and remaps every reference it walks. Once that traversal completes, the previous cycle's forwarding tables are released. That is why the phase is named "Concurrent Mark/Remap" — remapping is not a separate pass, it rides along on marking. ## Practical consequences - **No compaction pause.** Defragmentation, historically the most pause-prone GC activity, becomes concurrent work. - **Fragmentation is bounded** because compaction runs every cycle instead of being deferred to an emergency full collection. - **Mutators do some copying.** A request thread can pay for a few object copies, which appears as small latency jitter rather than as a pause. - **Headroom is required.** During relocation, both the old and the new copies exist and the application keeps allocating, so the collector needs free pages to copy into. Running the heap too close to full is what turns into allocation stalls.
- How can ZGC free a page while references into it still exist elsewhere in the heap?Because staleness is encoded in the reference's color, not in the old object. Any thread loading such a reference is intercepted by the load barrier before it dereferences anything, and redirected through the off-heap forwarding table. The old memory is therefore never read, so it can be reused immediately; only the forwarding table must survive until remapping completes.
- Why does ZGC evacuate a chosen subset of pages instead of sliding all live objects together like a classic compactor?Sliding compaction requires a globally consistent view of the whole heap and updates every reference, which is very hard to do concurrently. Evacuating a relocation set makes the work incremental and bounded: copy cost is proportional only to the live data in the sparsest pages, freed space is proportional to their garbage, and each page can be freed independently once emptied.
Evacuating a nearly-empty apartment block: move the handful of remaining tenants into a new building, hand back the whole block at once, and leave a forwarding-address desk running until the mail directory catches up.
saying these in an interview costs you the question
- Saying the old object holds a forwarding pointer in its header, so the old page must stay alive until all references are fixed.
- Assuming a stop-the-world pass updates all references after copying — remapping is lazy and folded into the next mark.
- Claiming ZGC compacts the whole heap every cycle; it evacuates a selected relocation set.
- Missing the CAS: describing relocation as if only GC threads copy, or as if duplicate copies could both be used.
- Forgetting that relocation needs free heap headroom to copy into.