The 30 second answer
G1, the default, splits the heap into regions and tries to meet a pause-time goal, two hundred milliseconds by default, while keeping good throughput. ZGC does almost all its work while the application runs, so pauses stay very short even on huge heaps, at the cost of some CPU and memory overhead. I'd keep G1 for general services and pick ZGC when tail latency really matters.
| G1 | ZGC | |
|---|---|---|
| Default | yes | no, -XX:+UseZGC |
| Heap layout | equal-sized regions, young and old | regions of varying sizes; generational in 21 with a flag |
| Pauses | targets -XX:MaxGCPauseMillis (default 200 ms) | very short and mostly independent of heap size |
| Compaction | during evacuation pauses | concurrent |
| Best for | most services | large heaps, strict latency |
Tuning: for G1, set the heap size and the pause goal, and avoid fixing the young-generation size, which overrides the pause-time logic. For ZGC, the most important setting is the max heap size (-Xmx), so the collector has enough headroom for the allocation rate.
[Opinion] ZGC usually costs some throughput and memory compared with G1. Measure p99 latency and CPU on your own workload before switching.
On Java 21 use -XX:+UseZGC -XX:+ZGenerational for generational ZGC (JEP 439); from Java 23 it is the default ZGC mode.
Sources: GC Tuning Guide: G1, GC Tuning Guide: ZGC, JEP 439: Generational ZGC