Why architecture matters here
Pause time is not just a performance number; for many systems it is a correctness-adjacent property. A trading system, an ad auction, a real-time bidding endpoint, an interactive API with a p99 latency SLO — all of these are defined partly by their worst pauses, not their average throughput. A collector that averages great throughput but freezes for 800 ms once a minute will fail an SLO that a slightly-lower-throughput collector with 2 ms pauses passes easily. Shenandoah exists because, for a large and growing class of services, the tail of the pause distribution is the metric that matters, and heap-size-proportional pauses make that tail unbounded as data grows.
The architectural significance is that Shenandoah decouples pause time from heap size. With generational or region collectors that still compact under a global pause, engineers are forced into an uncomfortable trade: keep the heap small to keep pauses short, even when the workload would benefit from more memory. Shenandoah removes that trade. Because evacuation is concurrent, you can run a very large heap for a large working set and still keep pauses tiny, letting memory sizing follow the data's needs rather than the collector's pause budget. That is a qualitatively different design freedom.
Nothing is free, and understanding the cost is part of understanding why the architecture is shaped as it is. Concurrency is paid for in two currencies: barrier overhead on every reference load, which slightly reduces raw throughput, and headroom, because the collector must reclaim memory concurrently while the application keeps allocating into the same heap. If allocation outruns collection, Shenandoah has to fall back to slower, more disruptive modes. So the trade Shenandoah offers is not 'lower pauses for nothing'; it is 'much lower pauses in exchange for some throughput and the discipline of leaving allocation headroom.' For latency-bound services that is an excellent bargain; for a batch job that only cares about total throughput it may not be.
Seeing this clearly also clarifies where Shenandoah fits among its peers. It occupies the same design space as other low-pause concurrent collectors: they all move the heavy work out of the pause and accept barrier cost and headroom requirements in return. The choice between them and the throughput-oriented collectors is fundamentally a choice about which resource you are most willing to spend — pause time, throughput, or memory — and Shenandoah is the answer when pause time is the scarce one. Framing the decision that way keeps teams from cargo-culting a collector and instead picking the one whose trade matches their SLO.
The architecture: every piece explained
Shenandoah manages the heap as a set of equal-sized regions, the same organizing idea modern collectors use: regions can independently be empty, holding live data, or chosen as evacuation targets, which lets the collector reclaim memory in pieces rather than all at once. On each cycle it selects a collection set — the regions with the most garbage, where evacuation yields the most reclaimed space for the least copying — and concentrates its work there.
The cycle has three concurrent heavy phases. Concurrent marking traces the object graph from the roots to find what is live, running alongside the application with a snapshot-at-the-beginning discipline so objects live at the start of the cycle are not collected even if the app rewrites references during marking. Concurrent evacuation copies the live objects out of the collection-set regions into fresh regions, freeing the source regions entirely. Concurrent update-references then walks the heap fixing pointers so they point at the new copies rather than the now-stale originals. All three run while mutator threads keep executing.
The correctness glue is the load-reference barrier and the forwarding pointer. Every object has a forwarding word: before evacuation it points to itself; when the object is copied, the word is atomically set to the new location. The load-reference barrier is a short piece of code the JIT inserts on reference loads: when the application reads a reference, the barrier ensures it resolves to the current copy by following the forwarding pointer if the object has moved. This is what lets evacuation happen under a running thread — any thread that touches a moved object is transparently redirected to the new one, so it never operates on a stale copy. Older Shenandoah versions used a 'Brooks pointer' indirection on every access; modern versions use a more efficient load-reference barrier, but the principle is the same: correctness maintained per-load instead of per-pause.
What remains are a few short stop-the-world pauses that bookend the concurrent phases: an initial-mark pause to scan thread roots and start marking, a final-mark pause to finish marking and choose the collection set, and brief init/final update-reference pauses. These are the only times the application actually stops, and each does a small, bounded amount of root-scanning-class work — which is precisely why their length does not scale with the heap. The heavy, heap-proportional work (marking every object, copying live data, fixing every pointer) all happens in the concurrent phases outside the pause. Understanding which work is in-pause versus concurrent is the key to reasoning about Shenandoah's latency behavior.
End-to-end flow
Follow a GC cycle under a steady low-latency service. The heap fills as the application allocates; when occupancy crosses the collector's trigger threshold, Shenandoah begins a cycle with a very short initial-mark pause — all mutator threads stop just long enough to scan their stack roots and enable the marking barriers, then resume. This pause is a fraction of a millisecond regardless of how big the heap is, because it only touches roots.
With the app running again, concurrent marking traces the live object graph. The application keeps allocating and mutating references; the snapshot-at-the-beginning invariant and the marking barrier ensure the collector's view of liveness stays consistent even as the graph changes underneath it. When marking completes, a brief final-mark pause finishes any remaining work and selects the collection set — the regions richest in garbage. The application stops only for that short, bounded final-mark, then resumes.
Now the defining phase: concurrent evacuation. Shenandoah copies live objects out of the collection-set regions into fresh ones, all while the application runs. Here the forwarding pointer earns its keep. Suppose a mutator thread holds a reference to object X and reads a field from it at the exact moment the collector is copying X. The load-reference barrier on that read follows X's forwarding pointer: if X has already been copied, the barrier resolves to the new copy and the thread reads the correct, current data; if not, it reads the original, and a later access will be forwarded once the copy exists. Either way the thread never sees a corrupt or stale object. Millions of such reads happen during evacuation and every one is quietly redirected — the concurrency the whole design promises, delivered one barrier at a time.
After evacuation the source regions are free, but pointers throughout the heap still refer to the old locations, so concurrent update-references sweeps the heap rewriting them to the new copies, bracketed by short init/final-update pauses. When it finishes, the cycle is complete: garbage regions are reclaimed, live data is compacted into fresh regions, and the application never stopped for more than a few short root-scanning pauses. Now consider the stress case: allocation is so fast that the app fills the heap before evacuation frees enough space. Shenandoah first applies pacing, gently slowing allocating threads to let the collector keep up. If that is not enough it escalates to a degenerate cycle — finishing the current GC under a stop-the-world pause — and in the worst case to a full GC, a classic long compacting pause. These fallbacks are the safety net, and seeing one in the logs is the signal that the heap is too small or allocation too hot for the concurrent collector to stay ahead. In the healthy steady state they never fire, and the service sees only the tiny bookend pauses.