Garbage Collection: G1GC, ZGC, Epsilon, and AppCDS
GC Changes Across Java 9–11
| Release | Change | JEP |
|---|---|---|
| Java 9 | G1GC becomes the default GC | JEP 248 |
| Java 9 | Unified GC logging (-Xlog:gc*) | JEP 271 |
| Java 10 | Parallel Full GC for G1 | JEP 307 |
| Java 10 | Application Class-Data Sharing (AppCDS) | JEP 310 |
| Java 11 | Epsilon: No-Op GC | JEP 318 |
| Java 11 | ZGC: 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
- Young GC — only evacuates young regions; runs in parallel using application-thread safepoints
- Concurrent Marking — marks live objects across old regions concurrently with application threads
- Mixed GC — evacuates young regions + a selected set of old regions; keeps pause times bounded
- 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 Flag | Java 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 |
Recommended production GC log configuration
-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:
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.
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
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.
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.
GC impact analysis: Compare application metrics with Epsilon vs G1/ZGC to quantify exactly how much GC contributes to latency.
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
- List classes: Run the application once and capture which classes are loaded.
- Dump archive: Run with the class list to produce a
.jsaarchive file. - 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
| Scenario | Recommended GC | Why |
|---|---|---|
| General-purpose, mixed workload | G1GC (default) | Balanced throughput/latency |
| Small heap (<256 MB) | SerialGC (-XX:+UseSerialGC) | Less overhead |
| Maximum throughput, batch processing | ParallelGC (-XX:+UseParallelGC) | Highest throughput |
| Latency-sensitive, large heap | ZGC (-XX:+UseZGC) | <1ms pauses |
| Benchmarking / no-GC batch | Epsilon | Eliminate GC noise |