What is the Shenandoah garbage collector in HotSpot, and what does it mean that its pause times are said to be independent of heap size?
answer
- -XX:+UseShenandoahGC — concurrent marking AND concurrent evacuation
- Region-based; collection set = most-garbage regions first
- Pauses scan roots only ⇒ scale with threads, not heap
- Work isn't free: paid as CPU + barrier throughput
- JDK 12 experimental, JDK 15 production (JEP 379)
basics
~20 sA region-based HotSpot collector that does both marking and compaction (evacuation) concurrently with the running application. Its stop-the-world pauses only scan roots and do bookkeeping, so they scale with the root set, not with heap size or live data. Enabled with -XX:+UseShenandoahGC.
solid answer
~60 sShenandoah is a low-pause, region-based collector in HotSpot, selected with `-XX:+UseShenandoahGC`. Its distinguishing property is **concurrent compaction**: it copies live objects out of regions and into new ones while application threads keep running, rather than doing the copying inside a pause. What still stops the world is small and bounded: an initial mark that scans thread roots and arms barriers, a final mark that drains marking buffers and picks the collection set, and short update-references checkpoints. All the expensive work — tracing the object graph, evacuating live objects, rewriting references across the heap — happens concurrently. That is what "pause independent of heap size" means: pause length tracks the **root set** (thread stacks, statics, JNI handles), which grows with thread count rather than with the heap. A 4 GB heap and a 100 GB heap can have similarly short pauses. It does *not* mean total GC work is independent of heap size — the concurrent phases still cost CPU proportional to live data, which is paid as throughput rather than as latency.
code
text · 9 lines$ java -XX:+UseShenandoahGC -Xlog:gc -Xmx8g -jar app.jar
[2.014s][info][gc] Trigger: Average GC time (12.4 ms) is above the time for allocation rate
[2.015s][info][gc] GC(3) Pause Init Mark 0.412ms
[2.061s][info][gc] GC(3) Concurrent marking 45.9ms
[2.062s][info][gc] GC(3) Pause Final Mark 0.688ms
[2.089s][info][gc] GC(3) Concurrent evacuation 26.4ms
[2.091s][info][gc] GC(3) Pause Init Update Refs 0.102ms
[2.118s][info][gc] GC(3) Concurrent update references 26.8ms
[2.119s][info][gc] GC(3) Pause Final Update Refs 0.240msgo deeper
Know the flag and the headline: marking and compaction both happen while the application runs, so pauses stay short.
Explain that pauses only handle roots and bookkeeping, and that concurrent work costs CPU and throughput instead.
Be precise about what scales with what — pauses with root set, concurrent work with live data — and name the trade being made.
Position it as a latency-for-throughput trade with a resource floor, and state the SLO and CPU headroom conditions that justify adopting it.
## The problem Shenandoah solves Moving collectors give you compaction, which eliminates fragmentation and makes allocation a pointer bump. The traditional price is that objects can only move while the application is stopped — otherwise a thread could read a reference to an object that has just been relocated and act on a stale copy. That is why classic collectors do evacuation inside a stop-the-world pause, and why their pauses grow with the amount of live data they must copy. Shenandoah removes that coupling by making **evacuation concurrent**. Application threads and the collector run at the same time while objects are being relocated, coordinated by barriers the JIT compiler inserts into application code. ## Region-based heap The heap is divided into equal-sized regions (the size is derived from heap size, or set with `-XX:ShenandoahRegionSize`). Each region is entirely free, entirely in use, or part of a **collection set** chosen for evacuation. Regions with the most garbage are chosen first — collecting them yields the most free space for the least copying. Regions that turn out to contain no live objects at all are simply reclaimed outright without copying anything, which is often a large share of the win. Regions also mean the collector can bound its own work: it decides how many regions to include in a cycle instead of being forced to process the whole heap at once. ## Where the pauses are One cycle contains a few short stop-the-world points, and each does work proportional to the **root set** rather than the heap: - **Init Mark** — stop threads, scan their stacks and other roots, set the global GC state so barriers become active. - **Final Mark** — drain the buffers that recorded concurrent mutation, finish marking, choose the collection set, evacuate the objects directly referenced by roots. - **Init/Final Update References** — short checkpoints that bound the reference-rewriting phase and recycle the evacuated regions. Everything between those points — concurrent marking, concurrent evacuation, concurrent reference updating, concurrent cleanup — runs while the application runs. ## Why "independent of heap size" is precise but narrow The claim is about **pause duration**, not about total collector work. Roots are thread stacks, static fields, JNI handles and similar; their size grows roughly with the number of threads, not with how much data the application keeps. So doubling the heap does not double the pause. Concretely, teams typically see pauses in the single-digit millisecond range across very different heap sizes. But concurrent work is still proportional to live data, and it is paid in two currencies: - **CPU**: collector threads run alongside the application, so a share of the machine's cores is spent on GC while requests are being served. - **Throughput**: the barriers that make concurrent movement safe execute on ordinary application code paths, costing a percentage of application throughput even when no GC is in progress. A collector cannot make work disappear; it can only move it out of the pause. Shenandoah trades throughput and CPU for latency, which is the right trade for latency-sensitive services and the wrong one for batch jobs maximising throughput. ## Availability and configuration Shenandoah appeared as an experimental collector in JDK 12 (JEP 189) and became a production feature in JDK 15 (JEP 379); it was also backported to JDK 11 in several distributions. It is built into most OpenJDK builds but has historically been omitted from some vendor builds, so "is it in this JDK" is a real first question — `java -XX:+UseShenandoahGC -version` answers it immediately. A generational mode was added later (JEP 404, `-XX:ShenandoahGCMode=generational`), which collects young objects more cheaply and reduces the collector's CPU cost on typical workloads. Basic operation needs very little configuration: choose the collector, set a heap, and log GC. The main levers are heap size (headroom for concurrent evacuation), the heuristic that decides when to start a cycle, and the number of concurrent GC threads. ## When to reach for it Use it when the tail latency of stop-the-world compaction is the problem you actually have: interactive services, trading and bidding systems, low-latency APIs with large or growing heaps. Do not reach for it to fix an application that is simply allocating too much or leaking — a concurrent collector will lose the race against a runaway allocation rate and fall back to stop-the-world work, which is worse than where you started.
- If pauses do not grow with heap size, what does grow with heap size?The concurrent work: marking must trace all live objects and evacuation must copy the live objects in the collection set, so collector CPU consumption scales with live data. That shows up as reduced application throughput and higher CPU usage rather than longer pauses, and it means a bigger heap needs proportionally more collector capacity to keep up with allocation.
- What does grow the pauses, if not the heap?The root set. Initial and final mark scan thread stacks, static fields, JNI handles and similar, so a process with thousands of threads and deep stacks has measurably longer pauses than one with a few dozen. Reducing thread count and stack depth is a real Shenandoah tuning lever, unlike shrinking the heap.
Renovating a hotel while guests stay: rooms are cleared and refurbished one block at a time with guests moved as they come and go, instead of closing the whole hotel for a weekend.
saying these in an interview costs you the question
- Saying Shenandoah has no stop-the-world pauses at all
- Claiming it reduces total GC work rather than moving work out of the pause
- Assuming it is generational by default — the original design collects the whole heap, with a generational mode added later
- Believing a low-pause collector fixes an excessive allocation rate or a leak