Skip to main content

JVM Garbage Collectors: A Field Guide

· 8 min read

The common garbage collectors in the JVM include Serial, Parallel, ParNew, CMS, G1, and ZGC. They differ in concurrency, pause times, throughput, and the scenarios they suit best. This post walks through each of the major collectors in turn.

Serial Collector

Flags: -XX:+UseSerialGC -XX:+UseSerialOldGC

The Serial collector is the most basic and oldest garbage collector in the JVM. It performs garbage collection on a single thread. During collection, the JVM triggers a STW (Stop The World) pause: all user threads are suspended and only resume once collection finishes.

Algorithmically, Serial uses mark-copy (Copying) in the young generation and mark-compact in the old generation. Since the entire collection runs on a single GC thread, the implementation is simple and stable, but collection efficiency drops off noticeably on large heaps or machines with many CPU cores.

Serial is therefore best suited to applications with small heaps and few CPU cores, such as the old Client-mode JVM or resource-constrained systems.

Parallel Scavenge Collector

Flags: -XX:+UseParallelGC -XX:+UseParallelOldGC

Parallel Scavenge is the multi-threaded version of the Serial collector, and its core goal is to maximize throughput. During collection it runs multiple GC threads in parallel, improving overall collection efficiency.

By default the number of GC threads roughly matches the CPU core count, though you can set it explicitly with -XX:ParallelGCThreads. Algorithmically, Parallel matches Serial: mark-copy in the young generation, mark-compact in the old generation.

Because Parallel cares about throughput rather than pause time, it is a great fit for background computation services and batch processing systems. For applications with medium-sized heaps (around 3–4 GB), it delivers solid performance.

ParNew Collector

Flag: -XX:+UseParNewGC

ParNew can be thought of as the multi-threaded version of the Serial collector, but it is rarely used on its own — its main role is to pair with the CMS collector. ParNew handles young generation collection, while CMS handles the old generation.

ParNew uses a multi-threaded parallel collection mechanism and, like the others, uses mark-copy in the young generation. Since CMS targets low pause times and Serial is single-threaded, real-world CMS deployments typically pair it with ParNew to speed up young generation collection.

The ParNew + CMS combination was widely used in early web services and other pause-sensitive systems.

CMS Collector

Flag: -XX:+UseConcMarkSweepGC

CMS (Concurrent Mark Sweep) collects the old generation, and its design goal is to minimize pause time during garbage collection. Its defining feature is that GC threads run concurrently with user threads, cutting down application pauses.

CMS uses the mark-sweep algorithm and relies on tri-color marking for reachability analysis. Its collection cycle typically has five phases:

  1. Initial Mark Triggers a STW pause, marking only objects directly referenced by GC Roots. Since these are few, this phase is very fast.
  2. Concurrent Mark Runs concurrently with user threads, traversing the entire object graph and marking all reachable objects.
  3. Remark Triggers another STW pause to correct any drift caused by reference changes during concurrent marking.
  4. Concurrent Sweep Runs concurrently with user threads, sweeping unmarked objects.
  5. Concurrent Reset Resets CMS's internal state to prepare for the next GC cycle.

CMS flow diagram:

CMS breaks collection down from one big step into several, splitting both marking and sweeping, and moves the most expensive part — marking objects — into a phase that runs concurrently with user threads. The application barely notices the pauses: only the initial mark and remark phases have very short STW windows, while everything else runs concurrently. That's how CMS achieves its goal of the shortest collection pauses. Note that shortest pause time does not mean shortest total collection time — CMS's overall collection actually takes longer than other collectors; it just keeps the pauses minimal.

G1 Collector

Flag: -XX:+UseG1GC

G1 (Garbage First) has been the default garbage collector since JDK 9, targeting large-memory server environments. Unlike traditional collectors that physically partition the heap into young and old generations, G1 divides the entire heap into many Regions.

Each Region can dynamically serve as an Eden, Survivor, or Old region as needed. G1 tracks the garbage ratio in each Region and prioritizes collecting the Regions with the most garbage and the highest collection payoff — hence the name "Garbage First".

Another key feature of G1 is its predictable pause-time model. You can set a target maximum pause with -XX:MaxGCPauseMillis, and G1 automatically adjusts its collection strategy to meet it.

The overall collection cycle includes initial mark, concurrent mark, final mark, and Region evacuation phases. Compared with CMS, G1 does a better job of reducing memory fragmentation, supporting large heaps, and controlling pause times, which is why it is so widely used in modern JVM deployments.

ZGC Collector

ZGC is a low-latency garbage collector introduced in JDK 11, with the core goal of millisecond-level GC pauses. In most cases, ZGC keeps STW pauses under 10ms, and stays stable even on terabyte-scale heaps.

ZGC's implementation rests on a few key techniques: colored pointers, load barriers, and a Region-based ZPage memory management scheme. With these, ZGC can perform most of its garbage collection work concurrently with user threads.

ZGC's main collection cycle includes concurrent marking, concurrent relocation, and concurrent reference remapping. Since the vast majority of the work happens in concurrent phases, the impact on application threads is tiny.

That makes ZGC ideal for servers with huge heaps, real-time systems, and latency-critical workloads like financial trading.

Comparing the Common GC Collectors

  • Serial: single-threaded collection, good for small applications
  • Parallel: multi-threaded collection, throughput first
  • ParNew + CMS: the low-pause combo (common on JDK 8)
  • G1: the modern JVM default, suited to large-memory services
  • ZGC: ultra-low-latency collector for terabyte-scale heaps
CollectorTypeThreadingMain AlgorithmPause ProfileBest For
SerialYoung + OldSingle-threadedCopying + mark-compactLong STW pausesSmall apps, single-core CPUs
Parallel ScavengeYoungMulti-threadedCopyingThroughput firstBackground computation, batch jobs
Parallel OldOldMulti-threadedMark-compactThroughput firstHigh-throughput services
ParNewYoungMulti-threadedCopyingShort pausesPaired with CMS
CMSOldConcurrentMark-sweepLow pauseWeb services, low-latency systems
G1Whole heap (Regions)Concurrent + parallelCopying + mark-compactPredictable pausesLarge-memory servers
ZGCWhole heap (Regions)Highly concurrentMarking + relocation<10ms pausesHuge heaps, low-latency systems

Bonus: How Golang GC Works

Go introduced its tri-color concurrent mark-and-sweep GC in version 1.5 and has kept refining it since. Today it is a mature system built on concurrent marking + concurrent sweeping + write barriers.

The core traits of Golang's GC:

  • Tri-color marking algorithm
  • Concurrent marking
  • Write barriers to keep object references consistent
  • Extremely short STW pauses (typically < 1ms)
  • Automatic GC triggering (based on heap growth ratio)

The main flow of a Golang GC cycle:

  1. STW Start: pause all goroutines and initialize the GC roots.
  2. Concurrent Mark: GC runs concurrently with user goroutines, traversing object references with tri-color marking.
  3. Mark Termination: a brief STW pause to ensure all objects are fully marked.
  4. Concurrent Sweep: reclaim unmarked objects, running concurrently with the program.

The Core Differences Between JVM GC and Golang GC

The JVM offers multiple garbage collectors (Serial, CMS, G1, ZGC, and so on) with generational collection, adapting to different scenarios through combinations of algorithms and tuning flags. Golang, by contrast, was designed from the start around a single tri-color concurrent mark-and-sweep system with no generations, achieving low-latency collection through concurrent marking, write barriers, and extremely short STW pauses — a simpler overall design with a much lower tuning cost.

Why doesn't Go do generational GC? Because the Go team concluded that generational GC brings little benefit to latency-sensitive systems while adding runtime complexity.

Wrapping Up

From Serial to ZGC, the through-line of JVM garbage collector evolution is a constant trade-off between throughput and pause time. Single-threaded Serial wins on simplicity and stability, Parallel chases throughput, ParNew + CMS traded concurrency for low pauses, G1 made pauses predictable through Region-based collection, and ZGC squeezed pauses down to milliseconds with colored pointers and load barriers. When choosing a collector, there's no need to chase the newest one — what matters is your application's heap size and latency requirements. And comparing all this with Golang's single, non-generational GC design makes the different trade-offs the two runtimes made between complexity and flexibility that much clearer.

COMMENTS