Definition
- ZGC – a scalable, low-latency, mostly-concurrent garbage collector whose stop-the-world pauses stay under a millisecond regardless of heap size.
- Colored pointers – GC metadata stored in spare bits of the 64-bit reference (Marked, Remapped, generation bits).
- Load barriers – small JIT-inserted code on every reference load that checks the color bits and fixes up relocated references.
- Self-healing references – the property that each reference is corrected at most once per cycle on first touch.
- Concurrent relocation – ZGC compacts the heap while the application runs, using load barriers to lazily repoint references.
- Generational ZGC – young/old generation split for efficiency (opt-in Java 21, default Java 23, exclusive Java 24).
Imagine renovating the bank while customers keep transacting. The crew moves one desk at a time in the background. They tag each file with a coloured sticker that says this file is being moved. When a teller picks up a file and sees the sticker, the teller instantly updates their note to the new location and carries on. The coloured stickers are colored pointers, the teller's instant update is the load barrier and self-healing, and the one-desk-at-a-time move is concurrent relocation.
Understanding the Concepts
What problem does ZGC solve?
Traditional collectors pause the entire application to move objects. As the heap grows, the pause grows. ZGC eliminates that correlation: pauses stay under a millisecond even on multi-terabyte heaps.
# Enable ZGC
java -XX:+UseZGC -Xmx64g -jar fraud-scoring.jar
# GC log shows sub-millisecond pauses
# [0.234s] GC(0) Pause Mark Start 0.015ms
# [0.345s] GC(0) Pause Mark End 0.012ms
# [0.456s] GC(0) Pause Relocate Start 0.008ms
How do colored pointers work?
On a 64-bit JVM, a heap reference does not need all its bits for the address. ZGC steals a few bits to store GC state. The address bits still point at the object; the color bits tell ZGC what phase-relevant work is pending.
64-bit reference layout in ZGC: ┌──────────────────────────────────────────────────────────────┐ │ Unused │ Finalizable │ Remapped │ Marked1 │ Marked0 │ Address│ │ bits │ 1 bit │ 1 bit │ 1 bit │ 1 bit │ 44 bits│ └──────────────────────────────────────────────────────────────┘ The color bits encode the GC phase: Marked0/1 : object has been reached during marking Remapped : reference points to the object's current location Finalizable: object is only reachable through a finalizer
How do load barriers and self-healing work?
Load barriers are small snippets of code the JIT inserts on every reference load. When the application reads a reference, the barrier checks the color bits.
Pseudocode of a ZGC load barrier:
Object ref = loadFromMemory(address);
if (ref.colorBits != CURRENT_GOOD_COLOR) {
ref = slowPath_fixup(ref); // look up new address, rewrite
}
// ref now points to current location
package com.bank.service;
import java.math.BigDecimal;
public class FraudScoringService {
// Under ZGC, every reference load has a barrier
// but the cost is near-zero when the color is current
private final TransactionGraph graph; // large, long-lived
public FraudScoringService(TransactionGraph graph) {
this.graph = graph;
}
public BigDecimal scoreTransaction(String txnId) {
// This reference load goes through the load barrier
// If the graph was relocated, the barrier fixes the ref
TransactionNode node = graph.getNode(txnId);
return node.calculateRiskScore();
}
}
How does concurrent relocation work?
ZGC selects pages that are mostly garbage, copies the few live objects out to fresh pages, and lets the load barriers lazily repoint any references that still point at the old location.
Concurrent relocation: Before: [Page-A: 10% live, 90% garbage] [Page-B: 80% live] ZGC picks Page-A (most garbage): 1. Copy live objects from Page-A to fresh Page-C (concurrent) 2. Any reference to old-Page-A objects → load barrier fixes it 3. Page-A is freed After: [Page-C: relocated live] [Page-B: unchanged] [Page-A: FREE]
What is Generational ZGC?
Generational ZGC adds young and old generations. Since most objects die young, collecting the young generation frequently and cheaply reclaims most garbage without scanning the whole heap.
# Java 21-22: opt-in generational mode
java -XX:+UseZGC -XX:+ZGenerational -Xmx64g -jar app.jar
# Java 23: generational is default
java -XX:+UseZGC -Xmx64g -jar app.jar
# Java 24: non-generational removed (JEP 490)
# ZGC is always generational
java -XX:+UseZGC -Xmx64g -jar app.jar
Generational ZGC timeline:
Java 15: ZGC production-ready (single-generation)
Java 21: Generational ZGC opt-in (-XX:+ZGenerational)
Java 23: Generational ZGC default
Java 24: Non-generational removed (JEP 490)
How is ZGC different from G1?
| Aspect | G1 | ZGC |
|---|---|---|
| Pause model | Bounded STW (100-200ms) | Sub-millisecond |
| Pause scales with heap? | Yes (somewhat) | No |
| Mechanism | Evacuate selected regions (STW) | Concurrent relocation + load barriers |
| Max practical heap | ~64 GB | Multi-terabyte |
| Throughput overhead | Low | Moderate (barriers + concurrent work) |
| Default since | Java 9 | Opt-in (Java 15+) |
What are common mistakes?
The most common mistake is enabling ZGC without providing enough heap headroom for concurrent allocation.
# WRONG: ZGC with heap too tight for concurrent work
java -XX:+UseZGC -Xmx2g -jar high-throughput-service.jar
# May cause allocation stalls!
# CORRECT: give ZGC headroom
java -XX:+UseZGC -Xms8g -Xmx8g -XX:+AlwaysPreTouch -jar app.jar
What should a senior engineer know?
A senior engineer monitors the allocation-stall count and pause percentile on the dashboard. At National Bank of India, ZGC was used for the real-time fraud-scoring service that held a multi-gigabyte transaction graph in memory and had to answer the payment switch within a tight latency budget.
# Monitor ZGC health
java -Xlog:gc*:file=zgc.log:time,level,tags \
-XX:+UseZGC -Xms64g -Xmx64g -XX:+AlwaysPreTouch \
-jar fraud-scoring.jar
# Check for allocation stalls
grep "Allocation Stall" zgc.log
Visual Overview
ZGC concurrent cycle:
──────────────────────────────────────────────────
Application threads keep running throughout
Phase 1: Pause Mark Start (< 1ms STW)
Root scanning
Phase 2: Concurrent Mark
Walk object graph, mark live objects
(application runs, load barriers active)
Phase 3: Pause Mark End (< 1ms STW)
Finalize marking
Phase 4: Concurrent Relocate
Copy live objects from garbage-rich pages
to fresh pages (application runs)
Load barriers fix references lazily
Phase 5: Pause Relocate Start (< 1ms STW)
Update root references
Total STW: < 1ms regardless of heap size
──────────────────────────────────────────────────
Colored pointer:
┌─────────┬──────────┬─────────┬─────────────────┐
│ unused │ Remapped │ Marked │ 44-bit address │
└─────────┴──────────┴─────────┴─────────────────┘
Key Takeaway
ZGC achieves sub-millisecond pauses by doing the expensive work: marking and relocation, concurrently with the application. Colored pointers store GC metadata in spare bits of the 64-bit reference. Load barriers check the color on every reference load, and if the object has moved, they transparently fix the reference. That self-healing means each reference is corrected at most once per cycle, spreading the cost smoothly.
Generational ZGC adds young and old generations so most collection targets short-lived objects without re-scanning the whole heap. It was opt-in in Java 21, default in Java 23, and the only mode in Java 24.
At National Bank of India, ZGC kept the fraud-scoring service's 99th-percentile pause under a millisecond on a 64 GB heap holding a live transaction graph, while the generational young collections absorbed the heavy churn of short-lived scoring contexts.
Remember one sentence: ZGC's colored pointers and load barriers let it move objects while the application runs, so pauses stay under a millisecond no matter how large the heap grows.
---