Java notes
G1 vs ZGC at JDK 17+: Choosing a Collector and Sizing the Heap
When G1 still wins, what ZGC costs in memory and CPU, and how to measure it yourself
What each collector optimizes
- G1 is the default collector since JDK 9. It balances throughput and pause time, and works well across a wide range of heap sizes.
- ZGC is built for low pause times that stay short regardless of heap size, using concurrent work. On JDK 17 it is non-generational, so it examines the whole heap each cycle. JDK 21 added a generational mode (JEP 439), which lowers memory and CPU overhead compared with the single-generation version JEP 439.
# JDK 17
-XX:+UseG1GC
-XX:+UseZGC
# JDK 21+ (generational ZGC)
-XX:+UseZGC -XX:+ZGenerational
Where the cost shows up
- Plan for more memory when upgrading. Moving a service from G1 on an older JDK to ZGC can require a noticeably bigger heap and more native memory. Budget for it in your container limits, or the kernel's OOM killer will make the decision for you.
- Watch RSS, not just heap. Total process memory includes metaspace, thread stacks, code cache, GC data structures, and direct buffers. A container limit equal to
-Xmxis almost always too small.
When G1 still wins
- Small to medium heaps where G1 pauses are already well inside your latency budget.
- Throughput-oriented batch jobs, where a few longer pauses cost less than constant concurrent overhead.
- CPU-constrained containers with very few cores, since concurrent GC threads need cores to run on.
When ZGC is worth it
- Large heaps or strict tail-latency targets where G1's pauses show up in your p99.
- Services with enough spare CPU and memory to give the collector room.
- Workloads where a measured test shows fewer long pauses and no allocation stalls.
Heap sizing rules of thumb
- Measure the live set (memory still reachable after a full collection) from GC logs.
- Start with a heap at roughly 2x to 3x the live set for concurrent collectors, then adjust from measurement.
- Set
-Xmsequal to-Xmxfor steady services, to avoid resize pauses. - Keep the container limit well above
-Xmxto leave room for native memory. - Always turn on GC logging in production:
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
A load generator you can run
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
public final class AllocationLoad {
public static void main(String[] args) {
int cacheEntries = Integer.parseInt(args.length > 0 ? args[0] : "2000000");
int operations = Integer.parseInt(args.length > 1 ? args[1] : "5000000");
Map<Integer, byte[]> cache = new HashMap<>();
for (int i = 0; i < cacheEntries; i++) {
cache.put(i, new byte[64]); // long-lived data
}
long[] latencyNanos = new long[operations];
ThreadLocalRandom rnd = ThreadLocalRandom.current();
for (int i = 0; i < operations; i++) {
long start = System.nanoTime();
byte[] shortLived = new byte[512 + rnd.nextInt(2048)]; // garbage
shortLived[0] = 1;
if ((i & 0x3FF) == 0) { // occasional churn in the cache
cache.put(rnd.nextInt(cacheEntries), new byte[64]);
}
blackhole += shortLived.length;
latencyNanos[i] = System.nanoTime() - start;
}
Arrays.sort(latencyNanos);
System.out.printf("p50=%dus p99=%dus p99.9=%dus max=%dus%n",
latencyNanos[operations / 2] / 1_000,
latencyNanos[(int) (operations * 0.99)] / 1_000,
latencyNanos[(int) (operations * 0.999)] / 1_000,
latencyNanos[operations - 1] / 1_000);
}
static volatile long blackhole;
}
for heap in 2g 4g 8g; do
java -Xms$heap -Xmx$heap -XX:+UseG1GC -Xlog:gc*:file=g1-$heap.log AllocationLoad
java -Xms$heap -Xmx$heap -XX:+UseZGC -Xlog:gc*:file=zgc-$heap.log AllocationLoad
done
What to chart
Failure modes
- A heap sized to the old collector's needs, causing allocation stalls under ZGC.
- A container limit too close to the heap size, causing out-of-memory kills from native memory.
- Judging by averages. GC problems live in the tail.
Takeaways
- ZGC reduces pauses by spending CPU and memory. G1 remains a strong default.
- Size the heap from the measured live set, and size the container from RSS.
- Decide with your own tail-latency numbers, not slogans.