skip to content

HotSpot's Serial collector compacts the old generation rather than leaving a free list behind. Walk through the phases of that mark-compact collection and explain how references are fixed up after objects move.

level: middleimportance: should knowfreq 28%

answer

  1. Mark → compute addresses → adjust pointers → move
  2. Forwarding address lives in the object header
  3. Preserved marks stack saves hash/lock headers
  4. Sliding compaction keeps allocation order
  5. Phases 2 and 4 scale with heap; 1 and 3 with live set

basics

~20 s

Four passes in one stop-the-world thread: mark live objects from the roots; compute each live object's new address and store it as a forwarding pointer in its header; walk roots and live objects rewriting every reference to the forwarded address; then slide the objects down to those addresses and restore headers.

solid answer

~60 s

The Serial old-generation collector is a **sliding (Lisp2-style) mark-compact**, run entirely in one stop-the-world thread: 1. **Mark** — trace from the roots (thread stacks, statics, JNI handles), setting a mark bit for every reachable object. Because the header word is later reused, any header with meaningful contents (identity hash, lock state) is saved on a *preserved marks* stack. 2. **Compute new addresses** — sweep the space in address order, accumulating a running destination pointer; each live object's future address is written into its own header as a **forwarding pointer**. Objects keep their relative order, hence "sliding". 3. **Adjust pointers** — walk the roots and every live object's reference fields, replacing each reference with the forwarding address read from the referent's header. After this pass every reference already points at where the object *will* be. 4. **Move (compact)** — copy each live object down to its computed address in address order, then restore preserved headers. Afterwards live data sits contiguously at the bottom and all free space is one block, so promotion and allocation are pointer bumps. The cost is four whole-space passes, single-threaded, proportional to heap size and live data.

go deeper

for a junior

Know that the old generation is compacted so free space stays contiguous, and that references are updated when objects move.

for a middle

Name the four phases in order and explain the forwarding pointer stored in the header.

for a senior

Connect the algorithm to observed behaviour: pause time scaling with live set and heap, and the absence of fragmentation-driven allocation failures.

for a principal

Use it as the reference point for what concurrent compactors must add — barriers, forwarding indirection, and race handling — to do the same work without stopping the world.

## Why compact at all A collector that merely marks and sweeps leaves the survivors where they are and threads the gaps onto a free list. That is cheap, but it fragments: after enough cycles the old generation can hold plenty of free bytes and still fail to satisfy a single large allocation or a promotion, because no individual gap is big enough. Compaction removes that failure mode entirely by pushing live objects together, leaving one contiguous free region. The price is that objects **move**, and every reference to them must be found and rewritten. The Serial old collector pays that price on every full GC. Its algorithm is the classic *sliding* compaction (often called Lisp2), chosen because it preserves the original allocation order of objects — which tends to keep related objects near each other and is friendly to the CPU cache — and needs no extra side table proportional to the heap. ## Phase 1 — Mark All application threads are stopped at a safepoint. Starting from the root set (thread stacks and registers, static fields, JNI global references, and similar), the collector performs a transitive traversal of the object graph, setting a bit in each reachable object's header to mark it live. A marking stack holds pending work; if it overflows, the collector falls back to rescanning, which is slower but bounded. A detail that matters: the object header (mark word) is about to be commandeered for forwarding addresses in the next phase, but for some objects it holds information that must survive — an installed identity hash code, or lock state for an inflated monitor. Those headers are pushed onto a **preserved marks** stack, paired with the object, and restored at the very end. ## Phase 2 — Compute new addresses The collector now walks the old generation **in address order**, keeping a running "next free destination" cursor that starts at the bottom of the space. For each live object it encounters, it records the current cursor value into that object's header as a **forwarding pointer** and advances the cursor by the object's size. Dead objects are skipped, so the cursor lags behind the scan position by exactly the amount of garbage passed so far. At the end of this pass, every live object knows where it is going, but nothing has moved yet and no reference has changed. Because the walk is in address order and the cursor is monotonic, relative order is preserved — objects slide down, they never reorder. ## Phase 3 — Adjust pointers Now the collector rewrites references. It visits every root and every reference field inside every live object; for each reference it loads the referent's header, reads the forwarding address stored there in phase 2, and writes that address back into the field. It also fixes references from *outside* the old generation that point into it — the young generation's live objects and any remembered-set-recorded slots. After this pass the heap is in a temporarily inconsistent but well-defined state: every reference names the object's *future* address, while the object still physically sits at its old one. Nothing may run except the collector until phase 4 completes. ## Phase 4 — Move Finally the collector walks live objects in address order again and copies each one down to its recorded destination. Because destinations are always at lower addresses than sources and the walk proceeds upward, a simple forward memory move is safe even when source and destination overlap. Preserved headers are then restored from the stack, and the space's top-of-allocation pointer is set just above the last moved object. ## What you are left with — and what it costs The old generation is now dense at the bottom with one contiguous free block above it, so the next promotion or large allocation is a pointer bump and cannot fail for fragmentation reasons. Allocation locality is preserved. The cost is that all four phases are stop-the-world and single-threaded. Phases 2 and 4 sweep the entire space (so their cost tracks *heap size*), while phases 1 and 3 track the *live set* and the number of reference fields in it. That is why full-GC pause time in the Serial collector rises roughly with how much live data you keep, and why the collector is a poor fit for multi-gigabyte heaps with large live sets — there is no second thread to split the work across, and nothing runs concurrently with the application. The same algorithm is what makes Serial predictable: no fragmentation-driven surprises, no concurrent-cycle races, no fallback modes. You get one behaviour, and it is easy to reason about.

  • Why does compaction need a separate pointer-adjustment pass instead of fixing references as objects are moved?
    References must all be updated before any object is read at its new location, and a reference can be discovered long after its referent has moved. Computing every destination first and then rewriting all references in one pass guarantees a consistent snapshot: after phase 3 every reference names the final address, so the physical move in phase 4 needs no further bookkeeping.
  • Where is the forwarding address stored, and what happens to the data that was in that word?
    It is written into the object's header (mark word), which is otherwise unused during the collection. If the header held meaningful state — an installed identity hash code or an inflated lock — the collector pushes the original header onto a preserved-marks stack alongside the object and restores it after the objects have moved.

Reshelving a library by first chalking each kept book's new shelf number on its spine, then updating every catalogue card to that number, and only then physically moving the books — so no card ever points at an empty gap.

saying these in an interview costs you the question

  • Describing Serial's old-generation collection as mark-sweep with a free list; it compacts
  • Claiming compaction reorders objects arbitrarily — sliding compaction preserves address order
  • Forgetting that references from outside the old generation (roots, young gen) must also be adjusted
  • Saying the object's identity hash code can change because the object moved — it is preserved and restored

context