Back to CI/CD Pipeline
Q10Question 10 of 56

What Shenandoah GC is: Brooks forwarding pointers, concurrent compaction: and what Generational Shenandoah (Java 25, JEP 521) added

Key Concepts
  • Shenandoah is Red Hat's low-pause, mostly-concurrent collector that performs concurrent compaction while the application runs.
  • The Brooks forwarding pointer is an extra reference-sized word per object that always points to the object's current location.
  • Read and write barriers follow the forwarding pointer so accesses always reach the current copy during relocation.
  • Stop-the-world pauses are limited to short init-mark and final-mark handshakes, independent of heap size.
  • Generational Shenandoah (JEP 521, Java 25) adds young/old generations so most work targets short-lived objects.
  • Enable with -XX:+UseShenandoahGC; generational mode with -XX:ShenandoahGCMode=generational on Java 25.

Definition

Shenandoah compacts the heap concurrently using a per-object forwarding pointer so references always reach the current copy, and Java 25's JEP 521 made it generational.
  • Shenandoah – a low-pause-time, mostly-concurrent garbage collector developed by Red Hat that performs concurrent compaction.
  • Brooks forwarding pointer – an extra reference-sized word per object that normally points to itself; when the object is relocated, it points to the new copy.
  • Concurrent compaction – relocating live objects and defragmenting the heap while the application keeps running.
  • Read/write barriers – code inserted on reference accesses that follows the forwarding pointer to the current copy.
  • Generational Shenandoah – JEP 521 (Java 25) adds young and old generations for more efficient collection of short-lived objects.

Shenandoah is a renovation crew that moves desks while customers keep working. Every file folder has a little current-address slip stapled to the front (the Brooks forwarding pointer). When the crew copies a folder to a new desk, they update that slip. Anyone picking up the folder follows the slip to the real current copy, so nobody ever reads a stale folder. Generational Shenandoah adds the idea that most folders are thrown away quickly, so the crew focuses on the fresh pile and rarely disturbs the long-term archive.

Understanding the Concepts

What is Shenandoah?

Shenandoah, like ZGC, aims for pause times that do not grow with heap size by doing marking and compaction concurrently with the application. It was developed by Red Hat and is available in OpenJDK.

BASH
# Enable Shenandoah
java -XX:+UseShenandoahGC \
     -Xmx32g \
     -jar card-auth-service.jar

# GC log shows short pauses
# [0.123s] GC(0) Pause Init Mark 0.234ms
# [0.456s] GC(0) Pause Final Mark 0.345ms

How does the Brooks forwarding pointer work?

Every object carries one extra reference-sized word at a fixed offset. Normally that word points back to the object itself. When Shenandoah relocates the object, it copies the object first, then sets the original's forwarding word to point to the new copy.

DIAGRAM
Brooks forwarding pointer:

  Normal state:
  ┌────────────────────────────┐
  │ fwd_ptr → self (this obj)  │
  │ [object data]              │
  └────────────────────────────┘

  During relocation:
  ┌────────────────────────────┐     ┌────────────────────────────┐
  │ fwd_ptr → new copy ────────│────→│ fwd_ptr → self             │
  │ [old object data]          │     │ [copied object data]       │
  └────────────────────────────┘     └────────────────────────────┘
   (old location)                     (new location)

  Any access follows the fwd_ptr → always reaches current copy
JAVA
package com.bank.service;

import java.math.BigDecimal;

public class CardAuthService {

    // Under Shenandoah, every reference access goes through a barrier
    // that follows the forwarding pointer to the current copy
    private final BinLookupTable binTable; // long-lived, rarely moved

    public CardAuthService(BinLookupTable binTable) {
        this.binTable = binTable;
    }

    public boolean authorize(String cardNumber, BigDecimal amount) {
        // This read goes through the barrier
        // If binTable was relocated, barrier follows fwd_ptr
        String issuer = binTable.lookupIssuer(cardNumber);
        return issuer != null && amount.compareTo(
            binTable.getLimit(issuer)) <= 0;
    }
}

How does concurrent compaction work?

Because Shenandoah evacuates live objects while the application runs, it reclaims and defragments without a long stop-the-world compaction. The only STW pauses are short init-mark and final-mark handshakes.

DIAGRAM
Shenandoah GC phases:
  1. Init Mark        (STW, short)   → start marking
  2. Concurrent Mark  (concurrent)   → walk object graph
  3. Final Mark       (STW, short)   → finish marking
  4. Concurrent Cleanup              → identify empty regions
  5. Concurrent Evacuation           → copy live objects
  6. Init Update Refs (STW, short)   → start ref updates
  7. Concurrent Update Refs          → fix references
  8. Final Update Refs (STW, short)  → finish ref updates
  9. Concurrent Cleanup              → reclaim regions

  STW total: < 5ms regardless of heap size

How is Shenandoah different from ZGC?

| Aspect | Shenandoah | ZGC |
|---|---|---|
| Developed by | Red Hat | Oracle |
| Forwarding mechanism | Brooks pointer (extra word per object) | Colored pointers (metadata in reference bits) |
| Barrier type | Read + write barriers | Load barriers only |
| Memory overhead | Extra word per object | Multi-mapped virtual memory |
| Max heap | Practical up to ~64 GB | Scales to terabytes |
| Generational | Java 25 (JEP 521) | Java 21 opt-in, Java 24 exclusive |
| Availability | OpenJDK (not in Oracle JDK) | All JDK distributions |

What did Generational Shenandoah add?

Generational Shenandoah, JEP 521 in Java 25, splits the heap into young and old generations. Since most objects die young, collecting the young generation frequently reclaims the bulk of garbage without touching the old generation every cycle.

BASH
# Enable Generational Shenandoah on Java 25
java -XX:+UseShenandoahGC \
     -XX:ShenandoahGCMode=generational \
     -Xmx32g \
     -jar card-auth-service.jar
TEXT
Benefits of Generational mode:
  - Improved CPU efficiency (skip long-lived objects)
  - Lower sustainable allocation rate threshold
  - Reduced heap headroom requirement
  - Same concurrent compaction, same low pauses

When should you use Shenandoah vs ZGC?

Both achieve similar low-pause goals by different mechanisms. The choice often depends on JDK distribution and heap size.

TEXT
Choose Shenandoah when:
  - Using OpenJDK (Red Hat, Amazon Corretto)
  - Heap up to ~64 GB
  - Want concurrent compaction with low pauses

Choose ZGC when:
  - Need multi-terabyte heap support
  - Want colored-pointer approach (no extra word per object)
  - Using Oracle JDK or any distribution

What are common mistakes?

The most common mistake is using Shenandoah without monitoring for degenerated or full GC events, which signal the collector is falling behind.

BASH
# WRONG: no monitoring, surprise degenerated GC in production
java -XX:+UseShenandoahGC -Xmx16g -jar app.jar

# CORRECT: monitor degenerated and full GC counts
java -XX:+UseShenandoahGC \
     -Xlog:gc*:file=shenandoah.log:time,level,tags \
     -Xmx16g -jar app.jar

grep -c "Degenerated GC" shenandoah.log
grep -c "Full GC" shenandoah.log

What should a senior engineer know?

A senior engineer monitors the pause 99th percentile and the degenerated/full GC counts, keeping them at zero in a healthy service. At National Bank of India, Shenandoah ran the card-authorization service where a long-lived BIN lookup table sat untouched in the old generation while heavy churn of authorization objects was collected by frequent young collections.

BASH
# Verify Shenandoah health
grep "Pause" shenandoah.log | awk '{print $NF}' | sort -n | tail -5
# All should be < 5ms

grep -c "Degenerated" shenandoah.log
# Should be 0

grep -c "Full GC" shenandoah.log
# Must be 0

Visual Overview

DIAGRAM
  Shenandoah concurrent compaction:
  ──────────────────────────────────────────────
  Application threads keep running throughout

  Init Mark (< 1ms STW) ──→ start marking
         │
  Concurrent Mark        ──→ walk object graph
         │
  Final Mark (< 1ms STW) ──→ finish marking
         │
  Concurrent Evacuation  ──→ copy live objects
         │                    (barriers follow fwd_ptr)
  Concurrent Update Refs ──→ fix all references
         │
  Concurrent Cleanup     ──→ reclaim freed regions

  Brooks forwarding pointer:
  ┌──────────┐    fwd_ptr    ┌──────────┐
  │ OLD copy │ ────────────→ │ NEW copy │
  │(stale)   │               │(current) │
  └──────────┘               └──────────┘
  Any read → barrier → follows fwd_ptr → NEW copy

Key Takeaway

Shenandoah is Red Hat's low-pause collector that performs concurrent compaction using the Brooks forwarding pointer: an extra word per object that always points to the current copy. Read and write barriers follow this pointer, so references always reach the right location even during relocation. The only stop-the-world pauses are short init-mark and final-mark handshakes, independent of heap size.

Generational Shenandoah, delivered as JEP 521 in Java 25, adds young and old generations so the collector focuses on short-lived objects and rarely touches the long-lived old generation. This improves efficiency and reduces required headroom.

At National Bank of India, Shenandoah ran the card-authorization service where the BIN lookup table lived untouched in the old generation while heavy churn of authorization objects was mopped up by frequent young collections, keeping the pause 99th percentile under a millisecond.

Remember one sentence: Shenandoah compacts the heap concurrently using a forwarding pointer per object, and Generational mode in Java 25 made it efficient enough to rival ZGC for most low-latency workloads.

---

Share
Use ← → arrow keys to navigate