Definition
- Serial GC – single-threaded, full stop-the-world collection; lowest overhead, best for tiny heaps and single-core environments.
- Parallel GC (Throughput) – multi-threaded stop-the-world collection; maximizes total throughput, best for batch work that tolerates pauses (default through Java 8).
- G1 (Garbage First) – region-based, mostly concurrent; bounded, predictable pauses via a pause-time target, best for large server heaps (default since Java 9).
- ZGC – colored pointers and load barriers; sub-millisecond pauses independent of heap size, scales to terabytes (production-ready Java 15+).
- Shenandoah – Brooks forwarding pointers and concurrent compaction; similar low-pause goals to ZGC by a different mechanism (Red Hat).
Imagine cleaning the paperwork clutter in National Bank of India's offices. Serial is one cleaner who makes everyone stop: fine for a tiny branch. Parallel is a whole cleaning crew that still pauses everyone, but finishes fast: good for overnight batch processing. G1 cleans one room at a time, mostly while staff keep working, pausing everyone only briefly. ZGC and Shenandoah are near-invisible cleaners who tidy around staff without ever stopping them: exactly what you want for a trading system where a one-second freeze is unacceptable.
Understanding the Concepts
What is garbage collection and why does the collector matter?
Garbage collection reclaims heap memory occupied by objects no longer reachable. The central tension is that doing this safely can require pausing the application: a stop-the-world pause. Different collectors manage that pause differently.
# Check which GC is active
java -XX:+PrintCommandLineFlags -version 2>&1 | grep UseGC
# Java 8 default:
-XX:+UseParallelGC
# Java 17+ default:
-XX:+UseG1GC
How does Serial GC work?
Serial GC uses a single thread and performs a full stop-the-world pause for every collection. It has the lowest overhead and memory footprint.
# Enable Serial GC for a small container
java -XX:+UseSerialGC \
-Xmx128m \
-jar account-validator.jar
Best fit: heap < 200 MB, single-core containers
Pause: grows with heap size
Default: auto-selected for single-CPU environments
How does Parallel GC work?
Parallel GC uses many threads for stop-the-world collection to maximize total throughput. It was the default through Java 8.
# Enable Parallel GC for a batch job
java -XX:+UseParallelGC \
-XX:ParallelGCThreads=8 \
-Xmx4g \
-jar nightly-settlement.jar
Best fit: batch processing, analytics, throughput-bound work
Pause: shorter than Serial for same heap, but still stop-the-world
Default: Java 8 and earlier
How does G1 work?
G1 divides the heap into many equal-sized regions and collects the regions with the most garbage first, aiming for a bounded pause-time target.
# G1 with a 100ms pause target
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-Xmx8g \
-jar transfer-service.jar
Best fit: server applications, 2-64 GB heaps
Pause: bounded by MaxGCPauseMillis (default 200ms)
Default: Java 9 onward
How do ZGC and Shenandoah achieve sub-millisecond pauses?
Both do marking and relocation concurrently with the application. ZGC uses colored pointers and load barriers; Shenandoah uses Brooks forwarding pointers.
# Enable ZGC (generational by default from Java 23)
java -XX:+UseZGC \
-Xmx64g \
-jar fraud-scoring.jar
# Enable Shenandoah (generational mode in Java 25)
java -XX:+UseShenandoahGC \
-XX:ShenandoahGCMode=generational \
-Xmx32g \
-jar card-auth.jar
Best fit: latency-critical services, large heaps (GB to TB)
Pause: sub-millisecond, independent of heap size
Trade-off: extra CPU and memory overhead for concurrent work
How do all collectors compare?
| Collector | Threads | Pause model | Heap range | Default | Best for |
|---|---|---|---|---|---|
| Serial | 1 | Full STW | < 200 MB | Single-core | Tiny containers |
| Parallel | Many | Full STW | 1-8 GB | Java 8 | Batch, throughput |
| G1 | Many | Bounded STW | 2-64 GB | Java 9+ | General server |
| ZGC | Concurrent | Sub-ms | 1 GB-16 TB | Opt-in | Low-latency |
| Shenandoah | Concurrent | Sub-ms | 1-64 GB | Opt-in | Low-latency |
What are the defaults by Java version?
Java 8: Parallel GC (throughput-oriented)
Java 9+: G1 GC (balanced)
Java 15: ZGC production-ready (opt-in)
Java 21: Generational ZGC opt-in
Java 23: Generational ZGC default mode
Java 24: Non-generational ZGC removed (JEP 490)
Java 25: Generational Shenandoah (JEP 521)
# Verify the default collector on your JVM
java -XX:+PrintFlagsFinal -version 2>&1 | grep -i "Use.*GC "
What are common mistakes?
The most common mistake is choosing a collector without measuring. Fashion-driven choices waste resources.
# WRONG: using ZGC for a batch job that tolerates pauses
# ZGC's concurrent overhead reduces throughput for no benefit
java -XX:+UseZGC -Xmx4g -jar nightly-settlement.jar
# CORRECT: Parallel GC maximizes throughput for batch
java -XX:+UseParallelGC -Xmx4g -jar nightly-settlement.jar
# WRONG: default G1 for a service that needs sub-ms pauses
java -Xmx64g -jar fraud-scoring.jar # G1 pauses grow with heap
# CORRECT: ZGC for sub-ms pauses on large heaps
java -XX:+UseZGC -Xmx64g -jar fraud-scoring.jar
What should a senior engineer know?
Collector choice must be measurement-driven. Define a latency SLO, measure the actual pause behaviour under realistic load, and only then decide. At National Bank of India, the nightly settlement uses Parallel GC for maximum throughput, the Transfer API uses G1 for bounded pauses, and the fraud-scoring engine uses ZGC for sub-millisecond pauses on a 64 GB heap.
# Enable GC logging for measurement
java -Xlog:gc*:file=gc.log:time,level,tags \
-XX:+UseG1GC \
-Xmx8g \
-jar transfer-service.jar
# Analyze GC pauses
grep "Pause" gc.log | awk '{print $NF}' | sort -n | tail -5
Visual Overview
Collector spectrum: throughput ←──────────────→ latency Serial Parallel G1 ZGC / Shenandoah ───────── ─────────── ────────── ────────────────── 1 thread many threads region-based concurrent mark+relocate full STW full STW bounded STW sub-ms STW tiny heap batch work general large heap, low latency Java version defaults: ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Java 8 │ │ Java 9+ │ │ Java 15+ │ │ Parallel │ │ G1 │ │ ZGC opt │ └──────────┘ └──────────┘ └──────────┘
Key Takeaway
The JVM offers several garbage collectors that trade off throughput, pause time, and resource use differently. Serial is for tiny heaps. Parallel maximizes throughput for batch jobs. G1, the default since Java 9, gives balanced bounded pauses for general server work. ZGC and Shenandoah deliver sub-millisecond pauses independent of heap size for latency-critical services.
The clean anchor for defaults is that Parallel was the default through Java 8 and G1 has been the default since Java 9, while ZGC and Shenandoah are opt-in. Generational ZGC became the only mode in Java 24, and Generational Shenandoah arrived in Java 25.
The judgement that separates a senior answer is that collector choice must be measurement-driven. At National Bank of India, the nightly settlement batch used Parallel, the customer-facing Transfer API used G1, and the real-time fraud engine used ZGC, each chosen based on measured latency requirements, not fashion.
Remember one sentence: G1 is the sensible modern default, and you only move to ZGC or Shenandoah when measurements show G1's pauses exceed your latency budget.
---