Part 11 of 16

Garbage Collection: G1GC, ZGC, Epsilon, and AppCDS

GC Changes Across Java 9–11

ReleaseChangeJEP
Java 9G1GC becomes the default GCJEP 248
Java 9Unified GC logging (-Xlog:gc*)JEP 271
Java 10Parallel Full GC for G1JEP 307
Java 10Application Class-Data Sharing (AppCDS)JEP 310
Java 11Epsilon: No-Op GCJEP 318
Java 11ZGC: Scalable Low-Latency GC (experimental)JEP 333

G1GC as Default (JEP 248, Java 9)

G1 (Garbage-First) replaced Parallel GC as the default on systems with ≥2 CPUs and ≥2 GB heap. If you were using Java 8 with Parallel GC and relying on its default behaviour, switching to Java 11 silently changes your GC — worth understanding.

G1GC design

G1 divides the heap into equal-sized regions (1–32 MB each, configurable). It does not have separate young and old generation areas at fixed boundaries. Instead, regions are dynamically assigned roles:

  • Eden regions: new allocations
  • Survivor regions: objects that survived young GC
  • Old regions: long-lived objects
  • Humongous regions: objects larger than 50% of a region

G1’s key property: it prioritises collecting regions with the most garbage first (“Garbage-First”), targeting a configurable pause time goal.

Key G1 tuning flags

# Set target max GC pause time (default: 200ms)
-XX:MaxGCPauseMillis=200

# Heap region size (default: auto-computed based on heap size)
-XX:G1HeapRegionSize=16m

# Trigger mixed GC when old gen occupancy exceeds this percentage (default: 45%)
-XX:InitiatingHeapOccupancyPercent=45

# Minimum and maximum heap
-Xms2g -Xmx4g

# Number of concurrent GC threads
-XX:ConcGCThreads=4

# Enable explicit GC to run G1 (rather than be ignored)
-XX:+ExplicitGCInvokesConcurrent

G1 GC phases

  1. Young GC — only evacuates young regions; runs in parallel using application-thread safepoints
  2. Concurrent Marking — marks live objects across old regions concurrently with application threads
  3. Mixed GC — evacuates young regions + a selected set of old regions; keeps pause times bounded
  4. Full GC (fallback) — serial full collection; indicates heap pressure; avoid by sizing correctly

Parallel Full GC for G1 (JEP 307, Java 10)

Before Java 10, G1’s Full GC was serial — a single-threaded stop-the-world pause that could pause a JVM for seconds on large heaps. Java 10 made Full GC parallel, using the same number of threads as young GC:

# Control parallel GC threads
-XX:ParallelGCThreads=8

This does not eliminate Full GCs, but dramatically reduces their worst-case pause time.


Unified GC Logging (JEP 271, Java 9)

Java 8 had a dozen incompatible GC logging flags. Java 9 replaced them all with -Xlog:

Migrating GC log flags

Java 8 FlagJava 9+ Equivalent
-verbose:gc-Xlog:gc
-XX:+PrintGCDetails-Xlog:gc*
-XX:+PrintGCDateStamps-Xlog:gc*::time
-Xloggc:gc.log-Xlog:gc*:file=gc.log
-XX:+PrintGCTimeStamps-Xlog:gc*::uptime
-XX:+PrintGCApplicationStoppedTime-Xlog:safepoint
-XX:+PrintTenuringDistribution-Xlog:gc+age*=debug
-XX:+UseGCLogFileRotation-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20m
-Xlog:gc*,gc+heap=debug,safepoint:file=/var/log/app/gc.log:time,uptime,pid:filecount=5,filesize=20m

This logs all GC activity with timestamps, rotates after 20 MB, and keeps 5 files.

Reading GC logs (Java 11 format)

[2024-05-04T10:15:32.001+0000][1.234s][gc,start] GC(42) Pause Young (Normal) (G1 Evacuation Pause)
[2024-05-04T10:15:32.015+0000][1.248s][gc       ] GC(42) Pause Young (Normal) (G1 Evacuation Pause) 512M->128M(1024M) 14.200ms

The format: [timestamp][uptime][tag] message — significantly more structured than Java 8 logs.


ZGC — Scalable Low-Latency GC (JEP 333, Java 11, Experimental)

ZGC is designed for applications that cannot tolerate GC pauses above a few milliseconds, even with multi-terabyte heaps.

ZGC characteristics

  • Pause times: typically <1ms, regardless of heap size (up to TBs)
  • Heap size: 8 MB minimum to 16 TB maximum
  • Platform: Linux/x64 only in Java 11 (expanded to Windows/macOS in Java 14)
  • Status: experimental in Java 11; production-ready in Java 15

Enabling ZGC

# Java 11 — experimental flag required
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

# Java 15+ — no experimental flag needed
-XX:+UseZGC

How ZGC achieves low pause times

ZGC uses two techniques unavailable in G1:

  1. Colored pointers: The upper bits of every 64-bit object reference are used as metadata (GC state flags). This allows the GC to remap references without stopping all application threads.

  2. Load barriers: Every object reference load inserts a small check. If the GC has moved the object since the reference was stored, the load barrier updates the reference on the fly — again, without a stop-the-world pause.

The result: most GC work (marking, relocation) runs concurrently. Only three short pauses exist per GC cycle — typically 0.5–2ms each.

ZGC tuning

ZGC is intentionally simple to tune:

# Set heap size — ZGC manages the rest automatically
-Xms4g -Xmx16g

# Control number of GC threads
-XX:ConcGCThreads=4

# Enable GC logging
-Xlog:gc*:file=zgc.log

When to use ZGC

  • Financial trading systems, gaming servers, or any latency-sensitive application
  • Heaps larger than 8 GB where G1’s pause times become unpredictable
  • Applications where 200ms G1 pauses are unacceptable

In Java 11, prefer G1GC for most applications. Use ZGC only when you have measured and confirmed that G1 pause times are causing SLA violations.


Epsilon GC — No-Op Garbage Collector (JEP 318, Java 11)

Epsilon GC allocates memory but never frees it. When the heap is exhausted, the JVM exits. This sounds useless — but it has specific, legitimate use cases.

Enabling Epsilon

-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC

When to use Epsilon

  1. Performance testing / benchmarking: Eliminate GC overhead entirely to isolate application performance. If your benchmark produces no garbage (or fits within heap), Epsilon gives you GC-free measurements.

  2. Short-lived batch jobs: A job that processes a fixed dataset, produces output, and exits can run without GC if it fits in heap — no need to pay GC overhead.

  3. GC impact analysis: Compare application metrics with Epsilon vs G1/ZGC to quantify exactly how much GC contributes to latency.

  4. Library testing: Verify that your library produces no unexpected garbage during a hot path.

# Run with a fixed 512MB heap; exit on OOM rather than GC
java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC \
     -Xms512m -Xmx512m \
     -jar my-benchmark.jar

Application Class-Data Sharing (AppCDS, JEP 310, Java 10)

AppCDS extends the JVM’s Class Data Sharing feature to application classes. It pre-processes and serialises class metadata into a shared archive that multiple JVM instances can map into memory — reducing startup time and memory footprint.

How AppCDS works

  1. List classes: Run the application once and capture which classes are loaded.
  2. Dump archive: Run with the class list to produce a .jsa archive file.
  3. Use archive: Subsequent runs map the archive, skipping class loading for archived classes.

Step-by-step setup

# Step 1: Generate the class list
java -XX:DumpLoadedClassList=classes.lst -jar myapp.jar

# Step 2: Create the shared archive
java -Xshare:dump \
     -XX:SharedClassListFile=classes.lst \
     -XX:SharedArchiveFile=myapp.jsa \
     -jar myapp.jar

# Step 3: Use the archive on startup
java -Xshare:on \
     -XX:SharedArchiveFile=myapp.jsa \
     -jar myapp.jar

Startup time improvement

For a typical Spring Boot application with ~8,000 loaded classes:

Without AppCDS:  ~3.2s startup
With AppCDS:     ~2.1s startup  (~35% improvement)

Results vary by application size, JVM version, and disk speed.

AppCDS in Docker

FROM eclipse-temurin:11 AS build
COPY target/myapp.jar /app/myapp.jar

# Generate archive at build time
RUN java -XX:DumpLoadedClassList=/app/classes.lst -jar /app/myapp.jar --quick-exit
RUN java -Xshare:dump \
         -XX:SharedClassListFile=/app/classes.lst \
         -XX:SharedArchiveFile=/app/myapp.jsa \
         -jar /app/myapp.jar --quick-exit

FROM eclipse-temurin:11-jre
COPY --from=build /app/myapp.jar /app/myapp.jar
COPY --from=build /app/myapp.jsa /app/myapp.jsa
ENTRYPOINT ["java", "-Xshare:on", "-XX:SharedArchiveFile=/app/myapp.jsa", "-jar", "/app/myapp.jar"]

Note: The archive is JVM-version-specific. Rebuilding the archive after any JVM update is required.


Choosing the Right GC

ScenarioRecommended GCWhy
General-purpose, mixed workloadG1GC (default)Balanced throughput/latency
Small heap (<256 MB)SerialGC (-XX:+UseSerialGC)Less overhead
Maximum throughput, batch processingParallelGC (-XX:+UseParallelGC)Highest throughput
Latency-sensitive, large heapZGC (-XX:+UseZGC)<1ms pauses
Benchmarking / no-GC batchEpsilonEliminate GC noise

What’s Next

Next: Flight Recorder and JVM Monitoring (JEP 328)