Java Garbage Collector Evolution (1995 – 2025)

The Java Engineer
By -
0

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 VersionDefault GCMajor ImprovementPause TargetKey Benefit
Java 5ParallelParallel GC multi-threadedtens of msHigh throughput
Java 9G1G1 becomes default< 200msBetter for large heaps
Java 11G1ZGC experimental< 10msUltra low pause
Java 15G1ZGC & Shenandoah production< 1msTrue low-latency
Java 21 LTSG1Generational ZGC< 1ms + better throughputBest of both worlds
Java 23+G1Generational ZGC mature< 1msRecommended 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 NameTypePause TimesUse CaseFlags
Serial GCStop-the-World100ms+Tiny apps-XX:+UseSerialGC
Parallel GCMulti-threaded10–100msBatch jobs-XX:+UseParallelGC
G1Region-based< 200msGeneral purpose (default)-XX:+UseG1GC
ZGCConcurrent< 1msLow latency services-XX:+UseZGC
Generational ZGCGenerational + concurrent< 1ms + better throughputFuture default (2025+)-XX:+UseZGC -XX:+ZGenerational
ShenandoahConcurrent< 1–3msLow 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="">

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?

WorkloadRecommended GCWhy
Microservices, REST APIsGenerational ZGC<1ms pauses + great throughput
Spring Boot appsGenerational ZGC or G1Safe & fast
Kubernetes / DockerZGC / ShenandoahFast startup, low RSS
Very large heapsZGCTerabyte-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

Tags:

Post a Comment

0Comments

Post a Comment (0)

#buttons=(Ok, Go it!) #days=(20)

Our website uses cookies to enhance your experience. Learn more
Ok, Go it!