ZGC stores garbage-collection metadata inside the 64-bit object reference itself — the technique known as colored pointers. What is kept in those bits, how does the JVM still dereference such a pointer correctly, and what constraints does the technique impose on the platform and on memory footprint?
answer
- Address + color in one 64-bit word
- Marked0 / Marked1 alternate → no mark-bitmap sweep
- Flipping the good color invalidates the whole heap in O(1)
- Multi-mapping (old) vs barrier-masked address (generational)
- 64-bit only; no compressed oops → 8-byte refs
basics
~20 sZGC puts GC state — marked, remapped, and in the generational design generation and remembered-set bits — into unused bits of every 64-bit heap reference. Barriers interpret and strip the color before use. It requires 64-bit addressing, bounds the address space, and rules out compressed oops.
solid answer
~50 sA colored pointer carries **both an address and GC state**. ZGC reserves bits of the 64-bit reference for a *color*: whether the object has been marked in the current marking cycle (two alternating mark colors, so the meaning of "marked" flips each cycle without rewriting any pointer), whether the reference has been remapped since the last relocation, and — in generational ZGC — which generation and remembered-set state applies. Correctness comes from the barriers: any reference loaded from the heap is checked against the current *good color* and, if stale, repaired before the application ever dereferences it. The non-generational implementation additionally used heap multi-mapping (mapping the same physical page at several virtual addresses so any color still resolved), which the generational design dropped in favour of barriers that produce a clean address. Costs: 64-bit only, a bounded maximum heap because the color steals address bits, and **no compressed oops** — every reference is 8 bytes, so small-heap footprint is worse than G1's 4-byte compressed references.
code
text · 10 lines# JDK 23+: ZGC, generational by default
java -XX:+UseZGC -Xmx16g -Xlog:gc -jar app.jar
# Compressed oops are silently unavailable under ZGC even below the 32 GB threshold:
java -XX:+UseZGC -Xmx16g -XX:+PrintFlagsFinal -version | grep UseCompressedOops
# bool UseCompressedOops = false {lp64_product}
# Compare with G1 on the same heap size:
java -XX:+UseG1GC -Xmx16g -XX:+PrintFlagsFinal -version | grep UseCompressedOops
# bool UseCompressedOops = true {lp64_product}go deeper
Recall the one-liner: ZGC hides GC bookkeeping bits inside each object reference, which is why it needs a 64-bit JVM and cannot use compressed oops.
Name what the bits encode (marked, remapped, generation) and explain that flipping which color is 'good' invalidates all old state at once.
Explain the relocation motivation — state in the reference lets a mutator detect staleness without touching the old object — plus the concrete footprint cost of losing compressed oops.
Position it as a deliberate trade of memory footprint and per-load barrier cost for heap-independent pause times, and factor the missing compressed oops into capacity and cost models for mid-size heaps.
## The idea A garbage collector constantly needs to know things *about a reference*: has the object it points to been marked live in this cycle, has it been moved, does it belong to the young or old generation. Most collectors keep that state on the side — mark bitmaps, card tables, forwarding headers in the object. ZGC keeps it **in the pointer**. On a 64-bit machine, virtual addresses are not really 64 bits wide; typical hardware uses 48 bits, and no realistic heap needs all of that. ZGC takes some of the unused bits of every heap reference and calls them the *color*. A reference is then a pair: an address plus a small tag describing the GC's view of the target. ## What the color encodes The canonical (non-generational) scheme used four metadata bits: - **Marked0** and **Marked1** — two alternating "this object is marked live" bits. Cycle *N* treats Marked0 as good, cycle *N+1* treats Marked1 as good. Alternating means the collector never has to sweep the heap to clear mark state; changing which bit counts as good instantly invalidates every previous mark. - **Remapped** — the reference has been verified to point at the object's current (post-relocation) location. - **Finalizable** — the object is reachable only through finalization, which lets the collector treat that path differently. At any moment exactly one combination is the **good color**. A pointer whose color differs is *bad* and must be repaired before use. The pauses that begin marking and relocation are precisely where the JVM flips which color is good — an O(1) operation that logically invalidates every colored pointer in a multi-terabyte heap at once. That trick is the heart of the "pause time independent of heap size" property. Generational ZGC extends the scheme: it needs per-generation mark and remap state plus remembered-set bits, so it uses a different, wider layout with the object address shifted and metadata in the low-order bits. The exact bit positions are an implementation detail that has changed between releases; what matters in an interview is *what* is encoded and *why*, not the numeric offsets. ## How a colored pointer is dereferenced A CPU cannot dereference an address with junk bits set, so the color must be gone by the time hardware sees it. Two mechanisms have been used: - **Multi-mapping** (the original non-generational implementation, on Linux). The same physical heap page is mapped into several virtual address ranges, one per color. Whatever color a reference carries, the address is still a valid mapping of the same memory. This costs virtual address space and made tools like `ps` report alarming virtual sizes, but resident memory is unaffected. - **Barrier-produced clean addresses** (generational ZGC). Multi-mapping was dropped; the load barrier masks/shifts the color away and hands the compiled code a plain address. This simplified the design and removed the confusing virtual-memory reporting. Either way, the invariant is the same: application code only ever dereferences a pointer that has passed through a barrier. ## The constraints you pay 1. **64-bit platforms only.** There are no spare bits in a 32-bit address, so ZGC simply does not exist there. 2. **A bounded heap ceiling.** Bits spent on color are bits not spent on address, which caps the maximum heap. That cap has been raised over releases and now sits far above any practical deployment (terabytes), so it is a theoretical rather than operational limit. 3. **No compressed oops.** This is the one that shows up in real capacity planning. Other collectors on heaps below roughly 32 GB store references as 4-byte compressed values; ZGC cannot, because it needs the full 64-bit word for the color. Reference-dense data structures therefore occupy noticeably more memory — a common rule of thumb is a double-digit percentage increase in footprint for pointer-heavy heaps — and you also lose the cache-density benefit of narrower references. (Compressed *class* pointers are a separate, unrelated optimization.) 4. **Barrier coupling.** Because state lives in pointers, every reference load from the heap must be checked. That is the load barrier, and its cost is the throughput tax of the design. ## Why this beats side tables for a concurrent compactor The deep reason ZGC colors pointers rather than objects: it wants to relocate objects *while the application runs*. If the "has this moved?" flag lived in the object header, a mutator holding a stale reference would have to reach the old object to discover the object moved — but the old copy's memory may already be reclaimed. Putting the state in the reference means the mutator can tell, from the reference alone, that it must consult the collector before touching anything. That is what lets ZGC free evacuated pages immediately instead of keeping them alive until every stale reference has been repaired.
- Why does ZGC use two alternating mark bits instead of one?With a single bit, the collector would have to clear the mark state of every reference or every object before each cycle — work proportional to the heap. With Marked0 and Marked1 the collector simply changes which bit counts as "marked" for this cycle, so all marks from the previous cycle become stale instantly and for free. The barrier repairs stale colors lazily as references are loaded.
- A team migrating a 12 GB-heap service from G1 to ZGC sees resident memory grow noticeably even though the live set is unchanged. Why?Below roughly 32 GB, G1 uses compressed oops — 4-byte object references. ZGC needs the whole 64-bit word for the color, so every reference becomes 8 bytes. In pointer-dense structures such as large maps, trees or object graphs with many fields, that alone can add a double-digit percentage to the live-set footprint, and it also reduces cache density.
Like a coloured wristband at a festival: the band both identifies you and shows which day it is valid for. Staff change the accepted colour at midnight, which instantly invalidates every band in the venue without touching a single person.
saying these in an interview costs you the question
- Saying the color bits are stored in the object header — they are in the reference, which is what makes stale references self-identifying.
- Claiming ZGC works on 32-bit JVMs or with compressed oops enabled.
- Assuming multi-mapping means the heap is stored several times; it maps the same physical memory at multiple virtual addresses.
- Treating the specific bit positions as the important part rather than what the color encodes and how the good color flips.
- Believing the color is stripped by the CPU automatically rather than by a barrier (or by the mapping).