skip to content
everything.gc.gcFundamentals

how garbage collection works

mark, sweep, compact, copy. and tri-color marking.

chapter
chapter 14.04
reading time
10 min read

every java collector does two jobs: mark (find everything reachable from the roots) and reclaim the rest. there are three ways to reclaim: sweep (free the dead in place), compact (slide the living together), copy (evacuate the living to empty space). real collectors mix and match.

and doing any of it while your app keeps running needs one more trick: write barriers.

the picture

you know how there are two kinds of people when it comes to cleaning a room? the ones who throw out the junk and leave everything else where it is, and the ones who push all the furniture against one wall so there's one big empty space. garbage collectors are like that, plus a third kind: the person who moves everything worth keeping into a fresh, empty apartment and just walks away from the old one.

  • sweep: throw out the junk, leave the furniture. quick, but now there are gaps everywhere (fragmentation).
  • compact: push everything together. one big free space, but you lifted every piece of furniture you kept.
  • copy: move the keepers to a new place. you only lift what you keep, but you're paying rent on two apartments.

here's all three on the exact same heap. same objects, same two roots, same garbage cycle (H and I point at each other).

algorithm =
023578101213141516171920CACHEorderADBHCJEIFKG
mark stackempty

the collector doesn't look for garbage. it looks for everything that's still reachable, starting at the roots. nothing is marked yet.

referencesbeing processedlivegarbageGC machinery: free list, pointers, forwarding
step 1/16
pick an algorithm and step through it. mark-sweep runs the full mark first, then sweeps. watch the last step of each: sweep leaves free holes that a 3-cell object can't fit in, compact leaves one big hole, copying never touches the garbage at all.

mark: the part they share

all three start the same way, the way the previous chapter showed: start at the roots, follow every reference, mark everything you reach. what's left unmarked is garbage. the mark costs time proportional to the live objects (and the references between them). garbage is never visited during marking. it's just whatever didn't get a mark.

one level deeper:where do the mark bits go?+

two options. put a bit in each object's header (the mark word, see object layout), or keep a separate bitmap with one bit per 8 bytes of heap: 1/64th of the heap's size. Serial's full GC marks in the header. the concurrent collectors keep mark bits in side bitmaps, so the GC can mark without writing into objects the app is busy using. that bitmap is part of the "GC structures" from the memory map: for a 512 MB heap, 8 MB of bitmap.

three ways to reclaim

sweep walks the whole heap, live and dead, and puts every dead object's space on a free list. nothing moves, which is simple and safe. but the free space ends up chopped into holes between survivors, and allocation becomes "search the free list for a hole that fits". in the figure, 8 cells are free and a 3-cell object still can't be placed. that's fragmentation, and the cure is to move things.

compact slides every live object down to one end of the heap. it takes three passes: work out where each object will land (its forwarding address), rewrite every reference to the new addresses, then move. the objects keep their order, and afterwards the free space is one block: allocation is a pointer bump again. the price is touching every live object several times.

copy splits memory into two halves. it copies each live object from the current half (from-space) to the empty one (to-space) the moment it finds it, leaving a forwarding address behind so later references get fixed up. the to-space itself is the work queue: a scan pointer chases a free pointer, and when it catches up, everything live has been copied. dead objects are never looked at. then the whole old half is thrown away in one go.

mark-sweepmark-compactcopying
work donemark live, sweep the whole heapmark live, move live (3 passes)copy live only
moves objectsnoyes, slides themyes, evacuates them
fragmentationyesnono
allocating afterwardssearch a free listbump a pointerbump a pointer
memory overheadnonenonehalf the space sits empty
where java uses itCMS's old gen (removed in java 14)Serial and Parallel full GCs, G1's full GCevery young gen; G1, ZGC and Shenandoah evacuate regions

see that last row? copying wins wherever most objects are dead, because its cost only depends on what survives. that's exactly the situation in a young generation (most objects die young). the "half the space sits empty" price gets paid in a smart way too: young gens copy into small survivor spaces, and region-based collectors like G1 copy into whichever regions happen to be free.

quick check
a 4 GB heap holds 100 MB of live objects. which approach does the least work to reclaim the rest?

doing it while the app runs

everything so far assumed the app is frozen while the GC works: a stop-the-world pause. the JVM brings every thread to a safepoint, does the work, lets them go. simple and correct, but the pause grows with the live set. mark a 10 GB live set with every thread stopped and your p99 latency is gone.

so modern collectors mark concurrently, on GC threads, while the app keeps running and keeps changing references. that breaks something. the classic way to think about it is tri-color marking:

  • white: not seen yet. at the end, white means garbage.
  • grey: seen, but its fields haven't been scanned yet. this is the to-do list.
  • black: seen and scanned. the GC is done with it and won't look again.

marking is done when there's no grey left. the danger is the app moving a reference behind the GC's back.

barrier =
GC threadis running this step
rootAwhiteBwhiteCwhiteDwhite

concurrent marking starts, and the app keeps running alongside it. every object starts white: not seen yet.

white: not seengrey: seen, fields pendingblack: scannedfreedbarrier machinery
step 1/9
step through it with no barrier first, and watch C at the end. then switch to SATB or incremental update and step again: same app, same timing, C survives.

an object only gets lost when both of these happen during marking:

  1. the app stores a reference to a white object into a black object (black never gets rescanned, so the GC misses it).
  2. the app destroys every path from a grey object to that white object (so the GC can't find it the normal way either).

a write barrier is a few instructions the JIT adds around reference stores, and each style of barrier breaks one of the two conditions:

  • incremental update breaks (1): when you store a reference into a black object, grey the target. CMS used a variant of this.
  • snapshot at the beginning (SATB) breaks (2): before you overwrite a reference, log the old value. everything reachable when marking started gets marked. G1 and Shenandoah do this.
an SATB pre-write barrier, for obj.f = v
if (marking_is_active) {           // a thread-local flag, cheap to check
    old = obj.f
    if (old != null)
        satb_queue.add(old)        // "this was reachable at the start, keep it"
}
obj.f = v                          // the store you actually wrote

that's the deal every concurrent collector makes: a little bit of cost on every reference store (and, for ZGC and Shenandoah, on reference loads too) in exchange for not stopping the world for the long part.

one level deeper:how does the GC know which stack slots are references?+

a moving collector has to update every pointer to an object it moves, so it must know exactly which values are references. an int that happens to look like an address must not be "updated". java's collectors are precise: at every safepoint, the JIT records an oop map saying which registers and stack slots hold references right now, and the interpreter can compute the same thing from the bytecode. that's a big reason threads have to reach a safepoint before a GC: it's only there that the JVM has a map of their frames. collectors without that information (some C/C++ GCs) have to be conservative, and conservative collectors can't move objects.

keylines
  • every collector marks from the roots. marking costs time per live object, never per dead one.
  • sweep frees in place and fragments. compact slides and stays tidy. copy only touches survivors but needs spare space.
  • copying is perfect when most objects are dead: that's every young generation.
  • pauses grow with the live set, not with the amount of garbage.
  • concurrent marking needs a write barrier, or the app can hide a live object from the GC.