Java's garbage collector (GC) is one of the most important parts of the JVM. It frees developers from manual memory management while constantly evolving to meet modern application demands: lower latency, better throughput, higher density in containers, and reduced pause times.
Let’s explore every major GC in Java history, how each new Java version improved it, and most importantly — test everything live right now on your machine.
1. The Four Eras of Java Garbage Collection
graph TD
A[Classic Era
Java 1.0 – 8] --> B[Serial, Parallel, CMS, G1
Focus: Throughput → Low pause]
C[Modern Low-Latency
Java 9 – 15] --> D[G1 default, Shenandoah, ZGC
Sub-millisecond pauses]
E[Ultra-Low Latency
Java 16 – 21] --> F[ZGC production, Shenandoah
Consistent <1ms pauses]
G[Generational & Highly Tunable
Java 22 – 25+] --> H[ZGC Gen, Shenandoah Gen
Best of both worlds]
style A fill:#ffccbc
style G fill:#c8e6c9
Timeline of Major GC Improvements
gantt
title Java Garbage Collector Evolution Timeline
dateFormat YYYY
axisFormat %Y
section Classic
Serial & Parallel GC :done, 1996, 12y
CMS Introduced :done, 2004, 10y
G1 Experimental :milestone, 2012
section Modern Era
G1 Becomes Default :done, 2014, 11y
section Low-Latency Era
ZGC Experimental :2018, 3y
Shenandoah Experimental :2019, 2y
ZGC & Shenandoah Production :done, 2020, 1y
section Future
Generational ZGC Stable :active, 2023, 10y
| Java Version | Default GC | Major Improvement | Pause Target | Key Benefit |
|---|---|---|---|---|
| Java 5 | Parallel | Parallel GC multi-threaded | tens of ms | High throughput |
| Java 9 | G1 | G1 becomes default | < 200ms | Better for large heaps |
| Java 11 | G1 | ZGC experimental | < 10ms | Ultra low pause |
| Java 15 | G1 | ZGC & Shenandoah production | < 1ms | True low-latency |
| Java 21 LTS | G1 | Generational ZGC | < 1ms + better throughput | Best of both worlds |
| Java 23+ | G1 | Generational ZGC mature | < 1ms | Recommended for most apps |
2. All Major Garbage Collectors Explained (2025)
flowchart TD
Start[Java GC in 2025] --> Q1{Your Priority?}
Q1 -->|Low Latency| Low[ZGC / Generational ZGC
<1ms pauses]
Q1 -->|Max Throughput| High[Parallel GC
Batch jobs]
Q1 -->|General Purpose| Gen[G1 or Generational ZGC
Safe default]
Q1 -->|Huge Heaps >64GB| Huge[ZGC / Shenandoah
Terabyte ready]
style Low fill:#e8f5e8
style Huge fill:#fff3e0
| GC Name | Type | Pause Times | Use Case | Flags |
|---|---|---|---|---|
| Serial GC | Stop-the-World | 100ms+ | Tiny apps | -XX:+UseSerialGC |
| Parallel GC | Multi-threaded | 10–100ms | Batch jobs | -XX:+UseParallelGC |
| G1 | Region-based | < 200ms | General purpose (default) | -XX:+UseG1GC |
| ZGC | Concurrent | < 1ms | Low latency services | -XX:+UseZGC |
| Generational ZGC | Generational + concurrent | < 1ms + better throughput | Future default (2025+) | -XX:+UseZGC -XX:+ZGenerational |
| Shenandoah | Concurrent | < 1–3ms | Low latency | -XX:+UseShenandoahGC |
How Each Garbage Collector Actually Works — Visualized
Serial GC: The Original Single-Threaded Pioneer
Everything stops. One thread does all the work.
graph TD
subgraph "Stop-The-World Pause"
A[Application Threads] -->|Frozen| STW[Full Pause]
STW --> Mark[Mark Phase
Single thread traces roots]
Mark --> Sweep[Sweep Phase
Single thread frees memory]
Sweep --> Compact[Compact Phase
Single thread defragments]
Compact -->|Resume| A
end
style STW fill:#ffebee,stroke:#f44336
Parallel GC: Multi-Threaded Muscle
Same algorithm — but with all your CPU cores.
graph TD
subgraph "Parallel Stop-The-World"
A[App Threads] -->|Pause| P[Parallel GC Threads]
P --> M1[Mark Thread 1]
P --> M2[Mark Thread 2]
P --> M3[Mark Thread 3...]
P --> S1[Sweep Thread 1]
P --> S2[Sweep Thread 2]
P --> C1[Compact Thread 1]
M1 & M2 & M3 --> DoneMark[Done Marking]
S1 & S2 --> DoneSweep[Done Sweeping]
C1 --> DoneCompact[Done Compacting]
DoneCompact -->|Resume| A
end
style P fill:#e8f5e8,stroke:#4caf50
CMS: The First Concurrent Collector
Most work happens while your app runs!
gantt
title CMS — True Concurrent Collection
dateFormat X
axisFormat %Ss
section Application Thread
Running the whole time :active, app, 0, 60000
section GC Thread
Initial Mark (STW) :crit, mark1, 5000, 3000
Concurrent Mark :mark2, after mark1, 25000
Remark (STW) :crit, remark, after mark2, 3000
Concurrent Sweep :sweep, after remark, 20000
G1: Region-Based Intelligence
Heap divided into regions — collects the "garbagiest" first.
flowchart LR
R1[Region 1: 95% garbage]
R2[Region 2: 80% garbage]
R3[Region 3: 20% garbage]
R4[Region 4: 5% garbage]
R1 -->|Priority 1| Collect[Collect & Evacuate]
R2 -->|Priority 2| Collect
R3 -.->|Low priority| Skip[Skip for now]
R4 -.->|Skip| Skip
Collect --> Free[Free entire region]
style R1 fill:#ffccbc,stroke:#e57373,stroke-width:3px
style R2 fill:#ffecb3,stroke:#ffd54f,stroke-width:2px
style R3 fill:#c5e1a5,stroke:#9ccc65
style R4 fill:#b2dfdb,stroke:#4db6ac
style Collect fill:#c8e6c9,stroke:#66bb6a,stroke-width:3px
style Free fill:#e1bee7,stroke:#ab47bc,stroke-width:2px
ZGC: Colored Pointers Magic
Objects move while your app is reading them!
graph TD
subgraph "Colored Pointers"
P1[Pointer: 0x0000abcd
Color bits: 00] -->|App reads| LoadBarrier[Load Barrier]
LoadBarrier -->|During relocation| P2[Pointer: 0x0012efgh
Color bits: 11 → Forwarded!]
LoadBarrier -->|Transparent to app| NewLocation[Returns new address]
end
App[Your Application Thread] -->|Never notices| P1
style LoadBarrier fill:#e8f5e8,stroke:#4caf50
Generational ZGC: The Best of Both Worlds
Young objects die fast → collect often. Old objects survive → collect rarely.
flowchart LR
subgraph Young["Young Generation (Eden + Survivors)"]
direction TB
A[99% of objects] -->|Die young| Minor[Minor GC
Very frequent
Concurrent & <1ms --="" b="" survive="">|Promoted| Old
end
subgraph Old["Old Generation"]
C[Long-lived objects] -->|Very rare| Major[Major GC
Still concurrent & <1ms c27b0="" caf50="" code="" color:="" e3f2fd="" end="" f3="" f3e5f5="" ff9800="" fff="" fill:="" major="" minor="" old="" stroke:="" young="">1ms>1ms>
Shenandoah vs ZGC: Two Ways to Solve the Same Problem
graph TD
Z[ZGC
Colored Pointers
Load Barriers]
S[Shenandoah
Brooks Pointers
Forwarding Pointers]
Both[ZGC & Shenandoah
Concurrent relocation
<1ms pauses
No STW compact]
Z --> Both
S --> Both
style Both fill:#c8e6c9,stroke:#4caf50,stroke-width:4px
3. Hands-On: Test Every GC Right Now!
public class GCTest {
private static final int MB = 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
System.out.println("Starting GC stress test...");
System.out.println("Using GC: " + System.getProperty("java.vm.name"));
var list = new java.util.ArrayList<byte[]>();
while (true) {
for (int i = 0; i < 100; i++) {
list.add(new byte[10 * MB]);
}
list.subList(0, 50).clear();
Thread.sleep(10);
}
}
}
Run these commands and watch the difference:
# Generational ZGC (smoothest)
java -Xmx4g -XX:+UseZGC -XX:+ZGenerational -XX:+PrintGCDetails GCTest
# G1 (default - you’ll see pauses)
java -Xmx4g -XX:+UseG1GC -XX:+PrintGCDetails GCTest
4. Which GC Should You Use in 2025?
| Workload | Recommended GC | Why |
|---|---|---|
| Microservices, REST APIs | Generational ZGC | <1ms pauses + great throughput |
| Spring Boot apps | Generational ZGC or G1 | Safe & fast |
| Kubernetes / Docker | ZGC / Shenandoah | Fast startup, low RSS |
| Very large heaps | ZGC | Terabyte-ready |
Listen: Why Generational ZGC Changes Everything
Why Generational ZGC is the future of Java performance (3-minute audio explanation)
Action Step: Run the test code above right now. You’ll never want to go back to G1 after seeing Generational ZGC in action!
Happy coding — and may your pauses be ever under 1ms! 🚀
Tested on OpenJDK 23 + Generational ZGC — November 19, 2025

