Parallel GC, G1 (the default since Java 9), ZGC's sub-millisecond pauses via colored pointers, Shenandoah, and how to actually choose between them.
Published September 23, 2026
Garbage Collection Fundamentals covered the shared vocabulary (generations, mark-sweep-compact, GC roots). This lesson covers the actual collector implementations that ship with the JVM, each making a different tradeoff between throughput and pause time.
Uses multiple threads to perform a stop-the-world collection (all application threads paused during GC) as fast as possible. It doesn't try to minimize individual pause durations — it tries to maximize total application throughput (time spent doing useful work vs time spent in GC), accepting occasionally longer pauses in exchange for lower overall GC overhead. This makes it a good fit for batch jobs — a nightly ETL pipeline or a report generator that cares about total wall-clock completion time, not about any single pause being noticeable to an end user (because there is no end user waiting in real time).
G1 ("Garbage First") divides the heap into many equally-sized regions rather than a small number of large, contiguous generational spaces — each region can independently be Eden, Survivor, or Old, and this flexibility is what lets G1 pick which regions to collect based on which ones have the most garbage ("garbage first" — literally collecting the regions that will free the most memory for the least work).
-XX:MaxGCPauseMillis: G1's signature feature — you specify a target maximum pause time, and G1 adjusts how much work it attempts per collection cycle (how many regions to collect) to try to stay under that target. It's a soft goal, not a hard guarantee — an application generating garbage faster than G1 can keep up with can still exceed it.ZGC does almost all of its work concurrently with the running application — marking and relocating live objects while application threads keep executing, rather than stopping them. The mechanism that makes concurrent relocation (not just marking) safe is colored pointers: ZGC steals a few unused bits in every 64-bit object reference to encode metadata about that reference's state (has this object been relocated? does it need remarking?) directly in the pointer itself. When the application reads a pointer, a lightweight check (a "load barrier") inspects those color bits and, if the object has moved, transparently redirects to its new location — this is what lets ZGC move objects while the application is still running and potentially holding references to them, without a stop-the-world pause for the relocation itself. The result: pause times measured in single-digit milliseconds or less, largely independent of heap size (a multi-terabyte heap and a small heap have similarly low pause times).
Developed separately (originally by Red Hat) but solving the same core problem: low-pause concurrent compaction. Shenandoah achieves concurrent compaction via a different mechanism than ZGC's colored pointers (a forwarding pointer stored directly in each object's header, read via a lightweight barrier on access) — different implementation technique, same fundamental goal of moving objects without a long stop-the-world pause.
| Workload | Collector |
|---|---|
| Throughput-sensitive batch job, no user waiting on individual latency | Parallel GC |
| General-purpose service, want a tunable pause-time target | G1GC (the default — right for most services) |
| Latency-critical service, large heap, sub-millisecond pauses matter | ZGC or Shenandoah |
The honest default answer for most Spring Boot services: G1 is already the default since Java 9 and is the right choice unless you have a specific, measured reason to reach for something else — ZGC/Shenandoah's concurrent-everything approach trades some CPU overhead (the load barriers on every reference read aren't free) for pause-time guarantees that most applications don't actually need.
Q: If ZGC has such low pause times, why isn't it the default over G1? A: The concurrent load-barrier mechanism has a real (if often small) CPU throughput cost paid on every reference access, and for applications where G1's pause times are already acceptable, that tradeoff isn't worth making — G1 remains the better general-purpose default; ZGC earns its cost specifically when pause time is the metric that matters most.
Q: How does G1's remembered-set mechanism avoid becoming its own overhead problem? A: Maintaining remembered sets isn't free — every cross-region reference write needs to update the target region's remembered set (via a write barrier) — but this cost is paid incrementally, spread across normal application execution, in exchange for avoiding a full-heap scan during every collection, which is the far larger cost it's designed to prevent.
Q: Can you switch collectors without recompiling the application?
A: Yes — the collector is selected via a JVM startup flag (-XX:+UseG1GC, -XX:+UseZGC, -XX:+UseParallelGC, -XX:+UseShenandoahGC), entirely independent of application code; switching collectors to test a different tradeoff is a deployment/ops change, not a code change.
Q: Is a smaller heap always better for pause times, or does that only apply to some collectors? A: It mainly applies to traditional stop-the-world collectors like Parallel GC, where pause time scales with how much of the heap needs scanning — ZGC's pause times are specifically designed to stay low largely independent of heap size, which is one of its main selling points for very large heaps where a traditional collector's pauses would scale up proportionally.