Why architecture matters here

ZGC is architectural because it inverts the traditional relationship between collection work and application pauses. In a classic stop-the-world collector, the expensive operations — tracing the live set, compacting the heap to reclaim fragmentation — happen with every application thread frozen, so pause time is a direct function of how much work there is to do, which grows with heap size and live-set size. ZGC's premise is that this coupling is not fundamental: with the right machinery you can do that same expensive work concurrently, overlapping it with normal execution, and reduce the stop-the-world portion to a small, bounded quantity that does not grow with the heap. That is why ZGC advertises pause times measured in fractions of a millisecond even on terabyte heaps — the pauses are decoupled from the amount of live data.

The hardest part of that decoupling is concurrent relocation, and understanding why it is hard explains the whole design. Marking concurrently is well-trodden; the real challenge is moving a live object to a new location while the program is running, because at the instant you copy the object, every existing reference to it still points at the old address, and the program might dereference one of those references at any time. You cannot afford to stop the world and rewrite every reference — that is exactly the pause you are trying to eliminate. So ZGC needs a way for the application to keep using old references safely even after the objects they point to have moved, and to repair those references incrementally rather than all at once. That requirement is what forces the colored pointer and load barrier into existence.

The colored pointer is the enabling trick. On a 64-bit machine a heap pointer does not need all 64 bits to address the heap, so ZGC uses a handful of otherwise-unused bits in the pointer to store metadata about the object it references: which marking generation the pointer belongs to, and whether the target has been relocated. The pointer thus carries a small, current statement of the collector's view of that object, travelling with the reference itself rather than living in a side table. This is what lets the load barrier make a fast, local decision on every load without consulting global structures in the common case — the answer to 'is this reference up to date?' is encoded in the reference.

The load barrier is where correctness is enforced, and its cost is the price of admission. Every time application code loads an object reference from the heap, the JIT-compiled load barrier checks the colored bits; if they are 'good' for the current phase, the load proceeds at nearly native speed, and if they are 'bad' — the object has moved, or needs marking — the barrier takes a slow path that heals the reference (looks up the new address, updates the pointer, marks the object) before returning it. The genius is that the application only ever sees healed, correct references, but the healing happens lazily and distributed across normal execution instead of in one giant pause. The trade is a small throughput cost on every reference load in exchange for pauses that no longer scale with heap size — a trade that is overwhelmingly worth it for latency-critical services, and the defining architectural bet ZGC makes.

Advertisement

The architecture: every piece explained

Start with the two actors on the top row. The mutator threads are your application's threads, running your code and allocating objects. The GC threads run ZGC's work — marking and relocation — concurrently, on other cores, while the mutators keep going. The two mechanisms that let them coexist safely sit between them: colored pointers carry GC metadata bits inside every heap reference, and the load barrier is the code injected on every reference load that reads those bits and repairs the pointer if needed. This is the entire foundation: metadata in the pointer, a checkpoint on every load.

The middle row is the concurrent work itself. Concurrent mark traces the object graph from the roots to find the live set, coloring pointers as it goes so a marked reference is distinguishable from an unmarked one. Concurrent relocate is compaction: ZGC selects the regions with the most garbage, and copies their surviving live objects into fresh regions, freeing the old ones wholesale. As it moves each object it records the move in a forwarding table — a per-region map from the object's old address to its new one. And because references scattered across the heap still point at old addresses, lazy remap is how they get fixed: the load barrier, on the next load of a stale reference, consults the forwarding table, learns the new address, updates the pointer in place, and hands the application the correct one. No global fixup pass is needed.

The third row holds the two mechanisms that make the moving parts physically work. The multi-mapped heap is a virtual-memory trick: ZGC maps the same physical heap page at several different virtual addresses, one per 'color', so that a pointer with a given metadata bit set still resolves to the same underlying object. This is what lets colored pointers be dereferenced directly without masking off the metadata bits on every access. The tiny stop-the-world pauses are what remains of the classic freeze: ZGC still needs a few very short synchronization points — most importantly to scan thread stacks and other roots at the start of a phase — but these are bounded by the number of roots, not the size of the heap, so they stay sub-millisecond however large the heap grows.

The ops strip ties the machinery to how you actually run it. Because relocation is concurrent, the collector is racing the application's allocation rate: if mutators allocate faster than GC can reclaim, ZGC can run out of free regions and must stall an allocating thread until memory is freed — the allocation stall, which is ZGC's characteristic failure symptom and the thing to watch. Giving the heap enough headroom and enough concurrent GC threads is how you keep the collector ahead of the mutators, which is why sizing the heap for allocation rate — not just for live-set size — and provisioning GC threads are the central operational levers.

ZGC — concurrent, region-based collection driven by colored pointers and load barriersalmost all work runs while the application keeps running; pauses are sub-millisecondMutator threadsyour applicationColored pointersmetadata in the pointerLoad barrierruns on every ref loadGC threadsconcurrent mark + relocateConcurrent marktrace the live setConcurrent relocatecompact live objectsForwarding tableold addr -> new addrLazy remapbarrier fixes stale refsMulti-mapped heapsame page, 3 virtual viewsTiny STW pausesonly root scan, boundedOps — watch allocation stall rate, set heap for headroom, tune concurrent GC threadstracecolorhealrunmapmoverecordoperateoperate
ZGC runs marking and relocation concurrently with the application. Metadata bits stored inside object pointers (colored pointers), read by a load barrier on every reference load, let the collector move objects and heal stale references lazily. A multi-mapped heap makes the colored addresses resolve to the same physical page, and the only stop-the-world work is a bounded root scan.
Advertisement

End-to-end flow

Walk a single collection cycle. ZGC decides to start a cycle because free memory has dropped toward a threshold or an allocation-rate heuristic predicts pressure. It begins with a very brief stop-the-world pause to scan the roots — the references held in thread stacks, static fields, and similar — coloring them for the new marking phase. This pause touches only the roots, so it is short and its length does not depend on how big the heap is. The instant the root scan finishes, the world resumes and the real work begins concurrently.

Now concurrent marking runs. GC threads trace outward from the colored roots, visiting reachable objects and marking them live, while mutators continue to run. Here the load barrier plays its marking role: when application code loads a reference whose color shows it has not yet been marked in this cycle, the barrier's slow path marks the target (or enqueues it for marking) before returning the reference. This is how the collector stays correct despite the application concurrently rewiring the graph underneath it — any reference the application actually touches gets caught by the barrier and accounted for. When marking completes, ZGC knows the live set and, region by region, how much garbage each region holds.

Next comes relocation. ZGC selects the regions with the most reclaimable space and begins copying their live objects into fresh regions, recording each object's old-to-new mapping in that region's forwarding table. This too runs concurrently. The subtlety is what happens when a mutator, mid-relocation, loads a reference to an object that has just been (or is being) moved: the load barrier sees the 'relocated' color, looks up the forwarding table, gets the new address, updates the in-heap reference so future loads are fast, and returns the corrected pointer. If the mutator races the GC to an object not yet copied, the barrier can even relocate it itself so the application never waits on a half-moved object. Either way the application only ever observes a consistent, correct address — it never sees an object in two places or a dangling old copy.

Finally, consider the moment things go wrong: the application is allocating faster than ZGC can reclaim. Free regions dwindle, and a mutator thread asks for memory ZGC does not yet have because relocation has not caught up. Rather than corrupt the heap or blow the pause budget with an emergency full collection, ZGC stalls that allocating thread — it blocks until the concurrent collector frees enough regions to satisfy the request. From the application's perspective this looks like a latency spike on the unlucky request, and if it happens often it is the signal that the collector is losing the race. The fix is not a bigger pause but more headroom or more GC threads: give ZGC memory to work with and cores to work on, so it stays comfortably ahead of the allocation rate and allocation stalls stay rare. This is the operational heart of running ZGC — you are not tuning pause length, you are keeping the concurrent collector ahead of your mutators.