Java Shenandoah GC Interview Questions and Answers | Concurrent Compaction, Brooks Pointer and Production Tuning

Master Shenandoah Garbage Collector with real interview questions, concurrent marking, concurrent evacuation, Brooks forwarding pointers, load-reference barriers, SATB, Generational Shenandoah, JVM tuning, GC logs, and production troubleshooting.


Introduction

Shenandoah GC is a low-pause-time Garbage Collector designed to perform most expensive memory-management operations concurrently with application threads.

Traditional collectors may stop application threads while they:

  • Mark live objects
  • Move objects
  • Compact the heap
  • Update object references
  • Reclaim memory

Shenandoah moves much of this work outside long Stop-The-World pauses.

Its most important capability is concurrent compaction.

This means Shenandoah can move live objects and reduce heap fragmentation while application threads continue executing.

Shenandoah is especially useful for:

  • Low-latency APIs
  • Spring Boot services
  • Large Java heaps
  • Financial applications
  • Payment systems
  • Large in-memory caches
  • Kafka processing applications
  • Interactive enterprise workloads
  • Applications with strict tail-latency requirements

Shenandoah was integrated into upstream OpenJDK through JEP 189 and became a production feature in JDK 15 through JEP 379. It remains an explicitly selected collector rather than the general JDK default.


Learning Objectives

After completing this article, you will understand:

  • What Shenandoah GC is
  • Why Shenandoah was introduced
  • Region-based heap architecture
  • Concurrent marking
  • Concurrent evacuation
  • Concurrent reference updating
  • Concurrent compaction
  • Brooks forwarding pointers
  • Load-reference barriers
  • SATB marking
  • Collection Sets
  • Degenerated GC
  • Full GC
  • Allocation failure
  • Pacing
  • Shenandoah heuristics
  • Generational Shenandoah
  • JVM configuration
  • GC log analysis
  • Container considerations
  • Production troubleshooting
  • Shenandoah versus G1GC
  • Shenandoah versus ZGC
  • Senior-level interview questions

Why Was Shenandoah Introduced?

Large Java applications can experience long Garbage Collection pauses when collectors perform object relocation and heap compaction during Stop-The-World events.

Long pauses can result in:

  • Request timeouts
  • Failed health checks
  • Kafka consumer lag
  • Slow API responses
  • Missed latency objectives
  • Unstable p99 response times
  • Poor customer experience

Collectors such as G1 perform concurrent marking, but much of their object evacuation still happens during Stop-The-World pauses.

Shenandoah was designed to perform:

  • Marking concurrently
  • Object evacuation concurrently
  • Reference updating concurrently
  • Heap compaction concurrently

Its pause times are therefore influenced more by root-processing work than by the complete heap size. OpenJDK describes Shenandoah as a regionalized collector that performs the bulk of its work concurrently, including compaction.


Enabling Shenandoah GC

Use:

java -XX:+UseShenandoahGC -jar application.jar

For example:

java \
  -Xms4g \
  -Xmx8g \
  -XX:+UseShenandoahGC \
  -jar payment-service.jar

Shenandoah became a production feature in JDK 15, so standard modern OpenJDK builds that include it do not require -XX:+UnlockExperimentalVMOptions for the normal collector mode.


Verify the Active Garbage Collector

Use:

java -Xlog:gc -XX:+UseShenandoahGC -version

For a running application:

jcmd <pid> VM.flags

You can also inspect startup flags:

java \
  -XX:+PrintCommandLineFlags \
  -XX:+UseShenandoahGC \
  -version

High-Level Architecture

flowchart TD
    Application["Application Threads"] --> Heap["Shenandoah Heap"]

    Heap --> Mark["Concurrent Marking"]
    Mark --> Select["Select Collection Set"]
    Select --> Evacuate["Concurrent Evacuation"]
    Evacuate --> Update["Concurrent Update References"]
    Update --> Reclaim["Reclaim Regions"]

    Application --> Barriers["GC Barriers"]
    Barriers --> Mark
    Barriers --> Evacuate
    Barriers --> Update

The application continues running during most of these phases.


Core Characteristics

Shenandoah is:

  • Concurrent
  • Region-based
  • Compacting
  • Low-pause
  • Primarily latency-oriented
  • Able to reclaim fragmented regions
  • Designed to decouple pause duration from total heap size
  • Supported in single-generation and modern generational modes depending on JDK version

The collector still contains short Stop-The-World phases, but expensive heap-wide work is largely concurrent.


Shenandoah Heap Architecture

Shenandoah divides the Java heap into multiple equal-sized regions.

flowchart TD
    Heap["Java Heap"] --> R1["Region 1"]
    Heap --> R2["Region 2"]
    Heap --> R3["Region 3"]
    Heap --> R4["Region 4"]
    Heap --> RN["Region N"]

    R1 --> Used["Used Region"]
    R2 --> Free["Free Region"]
    R3 --> CSet["Collection-Set Region"]
    R4 --> Humongous["Humongous Allocation"]

Each region contains:

  • Live objects
  • Dead objects
  • Free space
  • Metadata used by the collector

The region is the primary unit used for selecting memory to reclaim.


Region Example

+--------------+--------------+--------------+--------------+
| Used Region  | Free Region  | Used Region  | CSet Region  |
+--------------+--------------+--------------+--------------+
| Live + Dead  | Empty        | Mostly Live  | Mostly Dead  |
+--------------+--------------+--------------+--------------+

Regions containing high amounts of reclaimable garbage are candidates for evacuation.


Shenandoah Collection Lifecycle

A simplified non-generational Shenandoah cycle contains:

  1. Initial Mark
  2. Concurrent Mark
  3. Final Mark
  4. Concurrent Cleanup
  5. Concurrent Evacuation
  6. Initial Update References
  7. Concurrent Update References
  8. Final Update References
  9. Concurrent Cleanup

OpenJDK documents these phases as the regular Shenandoah cycle.


Collection Cycle Diagram

flowchart TD
    Start["Start Collection"] --> InitMark["Pause: Initial Mark"]
    InitMark --> ConcurrentMark["Concurrent Mark"]
    ConcurrentMark --> FinalMark["Pause: Final Mark"]
    FinalMark --> Cleanup1["Concurrent Cleanup"]
    Cleanup1 --> Evacuation["Concurrent Evacuation"]
    Evacuation --> InitUpdate["Pause: Initial Update Refs"]
    InitUpdate --> UpdateRefs["Concurrent Update References"]
    UpdateRefs --> FinalUpdate["Pause: Final Update Refs"]
    FinalUpdate --> Cleanup2["Concurrent Cleanup"]
    Cleanup2 --> Complete["Cycle Complete"]

Phase 1: Initial Mark

Initial Mark begins concurrent marking.

During this phase, Shenandoah:

  • Pauses application threads
  • Prepares the heap for concurrent marking
  • Scans GC roots
  • Establishes the initial marking state

The duration is primarily influenced by the root-set size.

GC roots include:

  • Thread stack references
  • Static fields
  • JNI references
  • Class-loader references
  • JVM internal references
  • References embedded in generated code

Phase 2: Concurrent Mark

During Concurrent Mark, Shenandoah traverses the object graph while application threads continue running.

flowchart TD
    Roots["GC Roots"] --> A["Object A"]
    A --> B["Object B"]
    B --> C["Object C"]

    X["Object X"] --> Y["Object Y"]

    C --> Live["Reachable Objects"]
    Y --> Garbage["Unreachable Objects"]

Objects reachable from GC roots are treated as live.

Objects not reachable from any root are eligible for reclamation.

Because application threads can change references during marking, Shenandoah uses barriers to preserve correctness.


Phase 3: Final Mark

Final Mark completes concurrent marking.

During this Stop-The-World phase, Shenandoah:

  • Drains pending marking queues
  • Rescans roots
  • Completes liveness information
  • Selects regions for evacuation
  • Creates the Collection Set
  • Prepares for concurrent evacuation

Final Mark can become longer when:

  • The application has a large root set
  • Marking queues contain substantial pending work
  • Weak-reference processing is expensive
  • Class unloading is significant

Phase 4: Concurrent Cleanup

After marking, Shenandoah can immediately reclaim regions containing no live objects.

Region A:
Live data = 0%

Result:
Region A can be reclaimed without object evacuation.

These completely empty regions are called immediate garbage regions.

Reclaiming them provides free memory before concurrent evacuation begins.


Phase 5: Concurrent Evacuation

Concurrent evacuation is Shenandoah's most important feature.

Live objects are copied from selected regions while application threads continue running.

flowchart LR
    Source["Collection-Set Region"] --> Live["Live Objects"]
    Live --> Copy["Concurrent Copy"]
    Copy --> Destination["Destination Region"]
    Source --> Reclaim["Source Region Reclaimed Later"]

Application threads may access an object while it is being relocated.

Shenandoah uses forwarding information and access barriers to ensure that application threads find the correct object location.


Phase 6: Initial Update References

The Initial Update References phase is a short Stop-The-World transition.

Its primary responsibilities are:

  • Confirm evacuation work has completed
  • Synchronize collector and application threads
  • Prepare the heap for concurrent reference updating

This is normally one of the shortest pauses in the collection cycle.


Phase 7: Concurrent Update References

Objects have been moved, but some references may still point to their previous locations.

During Concurrent Update References, Shenandoah scans the heap and updates stale references.

flowchart LR
    OldRef["Reference to Old Address"] --> Scan["Concurrent Heap Scan"]
    Scan --> NewRef["Update to New Address"]
    NewRef --> Object["Relocated Object"]

This work occurs while application threads continue running.

OpenJDK notes that this phase scans the heap linearly and that its duration is related to the number of objects in the heap rather than the shape of the object graph.


Phase 8: Final Update References

Final Update References is another short Stop-The-World phase.

During this phase, Shenandoah:

  • Updates remaining root references
  • Completes reference repair
  • Ensures no live reference points into evacuated regions
  • Makes evacuated regions safe to reclaim

Its duration is influenced mainly by root-set processing.


Phase 9: Final Concurrent Cleanup

The final cleanup phase reclaims Collection-Set regions after all references have been updated.

Before:

Collection-Set Region
+----------------------------+
| Old copies of moved objects|
+----------------------------+

After final update:

+----------------------------+
| Fully reclaimable          |
+----------------------------+

The regions return to the pool of available heap regions.


What Is Concurrent Compaction?

Compaction reduces fragmentation by moving live objects together and reclaiming sparsely used memory regions.

Before compaction:

+-------------------------------------------+
| Live | Dead | Live | Dead | Live | Dead   |
+-------------------------------------------+

After compaction:

+-------------------------------------------+
| Live | Live | Live | Free                 |
+-------------------------------------------+

Many collectors perform compaction during long Stop-The-World pauses.

Shenandoah performs most evacuation and reference updating concurrently, allowing it to compact the heap with shorter pauses.


What Is a Brooks Forwarding Pointer?

The traditional Shenandoah design associates an object with a forwarding reference, often explained using a Brooks pointer.

Conceptually:

Object
  |
  +--> Forwarding Pointer
           |
           +--> Current Object Location

Before relocation:

Forwarding pointer --> Original object

After relocation:

Forwarding pointer --> New object location

Application access barriers follow the forwarding information to locate the current copy.


Brooks Pointer Diagram

flowchart LR
    Reference["Application Reference"] --> Barrier["Load-Reference Barrier"]
    Barrier --> Forwarding["Forwarding Pointer"]
    Forwarding --> Current["Current Object Location"]

The forwarding pointer allows application threads and GC threads to coordinate while objects are moved concurrently.

Implementation details evolve across JDK versions, but the interview-level concept remains important:

An indirection or forwarding mechanism helps locate the current copy of an object during concurrent evacuation.


What Is a Load-Reference Barrier?

A load-reference barrier is JVM-generated logic executed when an object reference is loaded.

Example:

Customer customer = order.getCustomer();

Conceptually:

flowchart TD
    Load["Load Object Reference"] --> Barrier["Load-Reference Barrier"]
    Barrier --> Moved{"Object Relocated?"}
    Moved -->|No| Continue["Use Existing Address"]
    Moved -->|Yes| Resolve["Resolve Forwarded Address"]
    Resolve --> Continue

The barrier helps:

  • Detect whether an object is in the Collection Set
  • Follow its forwarding pointer
  • Assist with evacuation when required
  • Return the object's current location

Mutator Assistance

Application threads are also called mutator threads because they modify the object graph.

When a mutator encounters an object that should be evacuated, it may assist the collector by copying the object or resolving its new location.

This spreads some evacuation work across:

  • GC worker threads
  • Application threads

The assistance improves progress but can add latency to individual object-access operations.


What Is SATB?

Shenandoah's traditional default mode uses Snapshot-At-The-Beginning, or SATB, marking.

SATB treats the object graph that existed near the start of the marking cycle as the logical live snapshot.

Suppose an application changes a reference:

Customer previous = order.getCustomer();

order.setCustomer(new Customer());

The previous reference might still need to be considered by the current marking cycle.

A write barrier records relevant reference changes.

flowchart LR
    OldGraph["Object Graph at Mark Start"] --> Mark["Concurrent Mark"]
    Change["Application Replaces Reference"] --> Barrier["SATB Barrier"]
    Barrier --> Queue["Record Previous Reference"]
    Queue --> Mark

In Red Hat OpenJDK 21 documentation, the normal satb mode is described as the default Shenandoah mode.


Shenandoah Modes

Earlier Shenandoah releases provide several operating modes selected through:

-XX:ShenandoahGCMode=<mode>

Documented modes for Red Hat OpenJDK 21 include:

Mode Status Purpose
satb or normal Product/default Concurrent collector using SATB marking
iu Experimental Incremental Update marking
passive Diagnostic Stop-The-World mode for testing

These modes and their availability can vary by JDK distribution and version.


Normal or SATB Mode

Enable explicitly:

-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=satb

This is the standard concurrent-compacting Shenandoah mode in traditional non-generational releases.

It uses:

  • SATB marking
  • Concurrent evacuation
  • Concurrent reference updating
  • Low-pause collection

Incremental Update Mode

Example:

-XX:+UnlockExperimentalVMOptions
-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=iu

Incremental Update, or IU, uses a different marking-barrier strategy.

It tracks references added after marking begins rather than preserving the same logical beginning snapshot as SATB.

This mode is experimental in the cited JDK 21 Red Hat documentation and should not normally be selected for production without controlled testing.


Passive Mode

Example:

-XX:+UnlockDiagnosticVMOptions
-XX:+UseShenandoahGC
-XX:ShenandoahGCMode=passive

Passive mode performs Stop-The-World collections.

It is primarily useful for:

  • Collector testing
  • Diagnosing barrier overhead
  • Comparing concurrent and non-concurrent behavior
  • Estimating the application's live data size
  • Isolating performance anomalies

It is not intended as the normal low-latency production mode.


What Is a Collection Set?

A Collection Set, commonly called the CSet, is the group of regions selected for evacuation.

Collection Set
    |
    +-- Region 4: 80% garbage
    +-- Region 8: 75% garbage
    +-- Region 15: 90% garbage

Shenandoah copies live objects out of these regions.

After all references are updated, the regions can be completely reclaimed.


Collection-Set Selection

flowchart TD
    Regions["Analyze Heap Regions"] --> Garbage["Measure Live and Garbage Data"]
    Garbage --> Select["Select Collection Set"]
    Select --> Evacuate["Evacuate Live Objects"]
    Evacuate --> Update["Update References"]
    Update --> Reclaim["Reclaim Regions"]

A larger Collection Set may reclaim more memory but requires:

  • More evacuation work
  • More destination space
  • More reference updating
  • More concurrent CPU time

What Is Pacing?

Because Shenandoah performs collection concurrently, application threads can continue allocating memory while GC is running.

If application allocation is faster than memory reclamation, the heap may become exhausted before the cycle finishes.

Shenandoah uses pacing to slow allocation when necessary.

flowchart TD
    Allocate["Application Allocation"] --> Check{"Enough Free Memory?"}
    Check -->|Yes| Continue["Allocate Normally"]
    Check -->|Low Reserve| Pace["Temporarily Slow Mutator"]
    Pace --> GC["Allow GC to Catch Up"]
    GC --> Continue

Pacing attempts to prevent more severe allocation failures.

From the application's perspective, pacing may appear as:

  • Increased allocation latency
  • Reduced throughput
  • Temporary request slowdown
  • Higher thread wait time

Allocation Pressure

Shenandoah needs sufficient free memory to:

  • Accept new allocations
  • Copy live objects during evacuation
  • Handle temporary traffic spikes
  • Complete concurrent phases

Example:

Maximum heap:       16 GB
Live data:          13 GB
Available reserve:   3 GB

If the application consumes the remaining 3 GB before Shenandoah completes its cycle, the collector may enter a failure mode.

OpenJDK notes that Shenandoah performs better when there is enough heap to accommodate allocations while concurrent phases are running. Required headroom depends on both live-set size and allocation pressure.


What Is a Degenerated GC?

A Degenerated GC occurs when a concurrent Shenandoah cycle cannot make progress quickly enough.

Instead of immediately performing a complete Full GC, Shenandoah may stop application threads and finish the current collection cycle in a Stop-The-World mode.

flowchart TD
    Concurrent["Concurrent Collection"] --> Failure{"Allocation Failure?"}
    Failure -->|No| Continue["Continue Concurrently"]
    Failure -->|Yes| Degenerated["Degenerated GC"]
    Degenerated --> Finish["Finish Current Cycle STW"]
    Finish --> Resume["Resume Application"]

A Degenerated GC attempts to reuse work already completed by the concurrent cycle.

It is generally less severe than starting a Full GC from the beginning, but it can still cause a significant pause.


Common Causes of Degenerated GC

  • Heap too small
  • Insufficient evacuation reserve
  • Very high allocation rate
  • Large live-data set
  • Sudden traffic spike
  • Collector CPU starvation
  • Container CPU throttling
  • Humongous allocation pressure
  • Memory leak
  • Collection starting too late

Frequent Degenerated GCs indicate the collector is repeatedly failing to keep up.


What Is Shenandoah Full GC?

If concurrent and Degenerated collection cannot recover sufficient memory, Shenandoah may perform a Full GC.

A Full GC is:

  • Stop-The-World
  • Heap-wide
  • Expensive
  • Compacting
  • A last-resort recovery mechanism
flowchart LR
    Failure["Concurrent Failure"] --> Degenerated["Degenerated GC"]
    Degenerated --> Enough{"Enough Memory Reclaimed?"}
    Enough -->|Yes| Resume["Application Resumes"]
    Enough -->|No| Full["Full GC"]

Frequent Full GCs should always be investigated.


Causes of Full GC

  • Memory leak
  • Heap close to live-set size
  • Very high allocation pressure
  • Insufficient CPU for concurrent workers
  • Failed evacuation
  • Humongous objects
  • Fragmentation pressure
  • Explicit System.gc()
  • Native or metadata pressure associated with broader JVM cleanup

Shenandoah Heuristics

Shenandoah uses heuristics to decide:

  • When to start a collection
  • Which regions to collect
  • How aggressively to collect
  • How much free-space reserve is required

The heuristic can be selected with:

-XX:ShenandoahGCHeuristics=<name>

Common heuristic names may include:

  • adaptive
  • static
  • compact
  • aggressive

Availability and exact behavior can vary between JDK versions and distributions.


Adaptive Heuristics

Example:

-XX:ShenandoahGCHeuristics=adaptive

Adaptive heuristics use previous application and GC behavior to determine when collection should begin.

They consider signals such as:

  • Allocation rate
  • Free-memory level
  • Previous cycle duration
  • Recent allocation spikes
  • Evacuation requirements

This is generally the appropriate default approach for normal production workloads.


Static Heuristics

Example:

-XX:ShenandoahGCHeuristics=static

Static heuristics use more fixed thresholds to start collections.

This can be useful when:

  • Workload behavior is highly predictable
  • Testing specific thresholds
  • Reproducing performance behavior
  • Comparing collector strategies

It normally requires more manual understanding than adaptive behavior.


Compact Heuristics

Example:

-XX:ShenandoahGCHeuristics=compact

Compact heuristics attempt to reduce memory overhead, potentially at the cost of:

  • More frequent collections
  • Lower throughput
  • Less allocation headroom

This may help memory-constrained environments but must be tested carefully.


Aggressive Heuristics

Example:

-XX:+UnlockDiagnosticVMOptions
-XX:ShenandoahGCHeuristics=aggressive

Aggressive mode runs collections extremely frequently.

It is intended primarily for:

  • Testing
  • Stress testing
  • Collector verification
  • Finding concurrency bugs

It is not a normal production configuration.


Generational Shenandoah

Traditional Shenandoah operated as a single-generation collector.

That means recently allocated and long-lived objects participated in the same broad collection model.

Generational Shenandoah separates objects into:

  • Young generation
  • Old generation
flowchart TD
    Heap["Shenandoah Heap"] --> Young["Young Generation"]
    Heap --> Old["Old Generation"]

    Young --> Frequent["Frequent Young Collections"]
    Frequent --> Promote["Promote Survivors"]
    Promote --> Old

    Old --> Infrequent["Less-Frequent Old Collection"]

This design uses the generational hypothesis:

Most Java objects die young.


Why Generational Shenandoah?

A generational design can improve:

  • Sustainable throughput
  • Memory utilization
  • Allocation-spike resilience
  • Young-object reclamation efficiency
  • Collection frequency
  • CPU efficiency

Instead of repeatedly scanning and processing the complete heap, the collector can focus more frequently on recently created objects.


Generational Shenandoah Version Timeline

The status has changed across recent JDK releases:

JDK Generational Shenandoah status
JDK 24 Experimental through JEP 404
JDK 25 Product feature through JEP 521
JDK 26 Generational mode becomes the default through JEP 535

JEP 521 delivered Generational Shenandoah as a product feature in JDK 25 while retaining single-generation Shenandoah as the default for that release.

JEP 535 subsequently switched the Shenandoah default to generational mode and deprecated the non-generational mode.


Enabling Generational Shenandoah

For JDK 24 experimental mode:

java \
  -XX:+UseShenandoahGC \
  -XX:+UnlockExperimentalVMOptions \
  -XX:ShenandoahGCMode=generational \
  -jar application.jar

For JDK 25 product mode:

java \
  -XX:+UseShenandoahGC \
  -XX:ShenandoahGCMode=generational \
  -jar application.jar

JEP 521 removed the requirement for -XX:+UnlockExperimentalVMOptions when Generational Shenandoah became a product feature in JDK 25.

For JDK 26, selecting Shenandoah uses generational mode by default:

java \
  -XX:+UseShenandoahGC \
  -jar application.jar

Always verify exact behavior with the JDK build and vendor used in production.


Generational Collection Flow

flowchart TD
    Allocate["Allocate New Object"] --> Young["Young Generation"]
    Young --> YoungGC["Young Collection"]
    YoungGC --> Dead["Reclaim Dead Objects"]
    YoungGC --> Survive["Object Survives"]
    Survive --> Promote["Promote to Old Generation"]
    Promote --> Old["Old Generation"]
    Old --> Global["Old or Global Collection"]

Young collections reclaim short-lived objects without requiring equivalent work across all old objects.


Remembered Sets and Card Tracking

A generational collector must identify references from old objects to young objects.

Example:

Old Customer Object
        |
        +----> Young Address Object

Without tracking this relationship, the collector might need to scan the entire old generation during every young collection.

Generational Shenandoah therefore maintains metadata that helps locate cross-generation references.

flowchart LR
    Old["Old-Generation Object"] --> Young["Young-Generation Object"]
    Write["Reference Update"] --> Barrier["Store Barrier"]
    Barrier --> Metadata["Cross-Generation Metadata"]
    Metadata --> YoungGC["Young Collection"]

This improves collection efficiency but adds barrier and metadata overhead.


Shenandoah vs G1GC

Feature Shenandoah G1GC
Primary goal Low pause time Balance latency and throughput
Marking Concurrent Mostly concurrent
Object evacuation Concurrent Primarily Stop-The-World
Reference updating Concurrent Handled as part of evacuation and barriers
Compaction Concurrent Incremental Stop-The-World evacuation
Heap organization Regions Regions
Failure fallback Degenerated and Full GC Evacuation failure and Full GC
Throughput May have higher barrier cost Often stronger general-purpose throughput
Typical use Latency-sensitive services General enterprise workloads

G1 is often the safer general-purpose starting point.

Shenandoah is appropriate when measured G1 pauses fail to meet latency objectives.


Shenandoah vs ZGC

Feature Shenandoah ZGC
Goal Very low pause time Extremely low pause time
Heap organization Regions ZPages or logical heap regions
Relocation Concurrent Concurrent
Pointer strategy Forwarding or Brooks-style indirection Colored pointers and forwarding metadata
Access mechanism Load-reference barriers Load barriers
Reference updating Explicit concurrent update-reference phase in classic design Lazy/self-healing remapping model
Failure symptom Pacing, Degenerated GC, Full GC Allocation stalls and eventual failure
Vendor history Strong Red Hat development and support OpenJDK low-latency collector
Generational mode Product in JDK 25, default in JDK 26 Generational-only in modern releases

Both collectors should be benchmarked using the real application.


Throughput Trade-Off

Low latency is not free.

Shenandoah introduces runtime overhead from:

  • Load-reference barriers
  • Write barriers
  • Forwarding checks
  • Concurrent marking
  • Concurrent evacuation
  • Concurrent reference updating
  • Additional memory metadata
  • Mutator assistance

OpenJDK notes that concurrent barriers can cause measurable throughput loss, although the actual impact depends heavily on application behavior.


Root-Set Impact

Shenandoah pause time is not completely independent of application structure.

Short Stop-The-World phases still process roots.

Large root sets can be caused by:

  • Very high thread counts
  • Large thread stacks
  • Many JNI references
  • Extensive class-loader state
  • Large numbers of static references
  • Monitoring or profiling agents
  • Large code cache root sets

OpenJDK identifies root-set scanning and updating as major pause-time contributors.


Virtual Threads and Root Processing

Modern Java applications may use large numbers of virtual threads.

Virtual threads are designed to be lightweight, but the application should still monitor:

  • Number of mounted carrier threads
  • Stack-processing behavior
  • Safepoint times
  • Root-scanning cost
  • Retained task objects
  • ThreadLocal usage

Do not assume that switching to virtual threads automatically removes all GC-root overhead.


Heap Sizing

The most important heap options remain:

-Xms
-Xmx

Example:

-Xms4g
-Xmx12g

Shenandoah requires sufficient heap headroom for:

  • Application live data
  • New allocations
  • Concurrent evacuation
  • Traffic spikes
  • Object promotion
  • Failure avoidance

A heap sized too close to the live-data set can lead to:

  • Pacing
  • Degenerated GC
  • Full GC
  • OutOfMemoryError

Example Heap Headroom

Maximum heap:      12 GB
Live-data set:      7 GB
Normal allocation:  2 GB
Safety reserve:     3 GB

This configuration has more opportunity to complete concurrent work than:

Maximum heap:      12 GB
Live-data set:     11 GB
Free space:         1 GB

The correct reserve depends on:

  • Allocation rate
  • Collection-cycle duration
  • Live-set size
  • CPU resources
  • Traffic variability

Shenandoah in Kubernetes

Container memory consists of more than Java heap.

Container Memory
    |
    +-- Java Heap
    +-- Metaspace
    +-- Thread Stacks
    +-- Direct Buffers
    +-- Code Cache
    +-- GC Metadata
    +-- Native Libraries
    +-- JVM Internal Memory

Bad example:

resources:
  limits:
    memory: "8Gi"
-Xmx8g

This leaves almost no room for non-heap and native memory.

A safer starting point might be:

-Xms4g
-Xmx6g

for an 8 GiB container, but the appropriate ratio must be established through monitoring.


CPU Considerations

Shenandoah performs substantial work concurrently.

The application and GC threads compete for CPU resources.

A restrictive container configuration such as:

resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "1"

can cause:

  • Slower concurrent marking
  • Slower evacuation
  • Slower reference updating
  • Increased pacing
  • Degenerated GC
  • Lower throughput
  • Higher application latency

Low-pause collectors generally require adequate CPU availability.


Example Spring Boot Configuration

java \
  -Xms4g \
  -Xmx8g \
  -XX:+UseShenandoahGC \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/dumps/application.hprof \
  -Xlog:gc*,safepoint:file=/logs/shenandoah-gc.log:time,uptime,level,tags:filecount=10,filesize=20M \
  -jar customer-service.jar

For JDK 25 Generational Shenandoah:

java \
  -Xms4g \
  -Xmx8g \
  -XX:+UseShenandoahGC \
  -XX:ShenandoahGCMode=generational \
  -XX:+HeapDumpOnOutOfMemoryError \
  -Xlog:gc*,safepoint:file=/logs/shenandoah-gc.log:time,uptime,level,tags:filecount=10,filesize=20M \
  -jar customer-service.jar

Shenandoah GC Logging

Basic logging:

-Xlog:gc

Detailed GC logging:

-Xlog:gc*

Heuristic decisions:

-Xlog:gc+ergo

Summary statistics:

-Xlog:gc+stats

Red Hat documents these logging categories as key Shenandoah diagnostics.


Production Log Configuration

-Xlog:gc*,safepoint:file=/logs/shenandoah-gc.log:time,uptime,level,tags:filecount=10,filesize=20M

This captures:

  • GC cycle phases
  • Pause durations
  • Concurrent work
  • Heap occupancy
  • Failure events
  • Safepoint timings
  • Collector ergonomics

Simplified GC Log Example

GC(42) Concurrent reset
GC(42) Pause Init Mark 0.412ms
GC(42) Concurrent marking 24.816ms
GC(42) Pause Final Mark 0.913ms
GC(42) Concurrent cleanup
GC(42) Concurrent evacuation 8.621ms
GC(42) Pause Init Update Refs 0.041ms
GC(42) Concurrent update references 12.442ms
GC(42) Pause Final Update Refs 0.387ms
GC(42) Concurrent cleanup

Interpretation:

  • Stop-The-World pauses are explicitly labeled Pause.
  • Marking, evacuation, cleanup, and reference updating occur concurrently.
  • Pause durations are much shorter than the complete collection-cycle duration.
  • The complete cycle may consume tens of milliseconds while application threads continue running.

Exact log wording varies by JDK release.


Important Metrics

Monitor:

  • Initial Mark pause
  • Final Mark pause
  • Initial Update References pause
  • Final Update References pause
  • Concurrent Mark duration
  • Concurrent Evacuation duration
  • Concurrent Update References duration
  • Allocation rate
  • Live-data set
  • Heap occupancy after GC
  • Pacing delays
  • Degenerated GC count
  • Full GC count
  • Collection frequency
  • GC CPU consumption
  • Safepoint time
  • Root-scanning time
  • Container CPU throttling
  • Application p95 and p99 latency

Frequently Asked Interview Questions

Question 1: What is Shenandoah GC?

Answer

Shenandoah is a low-pause, region-based, compacting Garbage Collector.

It performs most marking, evacuation, reference updating, and compaction concurrently with application threads.


Question 2: What is the main advantage of Shenandoah?

Answer

Its main advantage is concurrent compaction.

Live objects can be moved and fragmented regions reclaimed without pausing the application for the entire compaction process.


Question 3: Is Shenandoah completely pause-free?

Answer

No.

Shenandoah has several short Stop-The-World phases, including:

  • Initial Mark
  • Final Mark
  • Initial Update References
  • Final Update References

However, most expensive heap work occurs concurrently.


Question 4: Is Shenandoah pause time completely independent of heap size?

Answer

Not completely.

The design prevents pause time from scaling directly with complete heap-compaction work.

However, pause duration can still be affected by:

  • Root-set size
  • Thread count
  • JNI references
  • Class-loader state
  • Pending marking work
  • Weak-reference processing
  • Safepoint delays

Question 5: What is concurrent evacuation?

Answer

Concurrent evacuation copies live objects from selected regions while application threads continue running.

Barriers and forwarding information ensure that object references resolve to the correct current object location.


Question 6: What is concurrent compaction?

Answer

Concurrent compaction moves live objects together and reclaims fragmented regions without performing the entire operation during a long Stop-The-World pause.


Question 7: What is a Brooks forwarding pointer?

Answer

A Brooks-style forwarding pointer is an indirection associated with an object that identifies its current location.

Before movement, it points to the original object.

After relocation, it points to the new copy.


Question 8: What is a load-reference barrier?

Answer

A load-reference barrier executes when application code loads an object reference.

It checks whether the object has moved and resolves the reference to the object's current location.


Question 9: What is mutator assistance?

Answer

Application threads may assist the collector when they access an object requiring evacuation or reference repair.

This helps collection progress but can add latency to the application's object-access operation.


Question 10: What is SATB?

Answer

SATB means Snapshot-At-The-Beginning.

It is a concurrent-marking strategy that preserves the logical reachability view established at the beginning of the marking cycle.

Reference-change barriers ensure objects are not missed.


Question 11: What is the Collection Set?

Answer

The Collection Set is the group of regions selected for evacuation.

Live objects are copied out of those regions, references are updated, and the regions are then reclaimed.


Question 12: What is Concurrent Update References?

Answer

After evacuation, some references still point to old object locations.

During Concurrent Update References, Shenandoah scans the heap and changes those references to the relocated object addresses.


Question 13: Why are there Initial and Final Update References pauses?

Answer

Initial Update References transitions the collector from evacuation into reference updating.

Final Update References completes root-reference updates and confirms that evacuated regions can safely be reclaimed.


Question 14: What is pacing?

Answer

Pacing temporarily slows application allocation when the collector needs more time to reclaim memory.

It attempts to prevent severe allocation failure.


Question 15: What is a Degenerated GC?

Answer

A Degenerated GC occurs when a concurrent cycle cannot complete quickly enough.

Shenandoah pauses application threads and finishes the current collection cycle using Stop-The-World execution.


Question 16: Is a Degenerated GC the same as Full GC?

Answer

No.

A Degenerated GC attempts to finish the current partially completed cycle and reuse its work.

A Full GC performs a broader heap-wide recovery and compaction operation.


Question 17: What causes Degenerated GC?

Answer

Common causes include:

  • Insufficient heap headroom
  • High allocation rate
  • Large live-data set
  • CPU starvation
  • Collection starting too late
  • Humongous allocations
  • Memory leaks

Question 18: What causes Shenandoah Full GC?

Answer

A Full GC may occur when concurrent and Degenerated collection cannot reclaim sufficient memory.

Typical causes include:

  • Heap exhaustion
  • Memory leak
  • Failed evacuation
  • Very high allocation pressure
  • Insufficient CPU
  • Large retained object set

Question 19: Does Shenandoah prevent memory leaks?

Answer

No.

It can only reclaim unreachable objects.

Objects retained by static collections, unbounded caches, queues, listeners, or ThreadLocal values remain live.


Question 20: What is Generational Shenandoah?

Answer

Generational Shenandoah separates the heap into young and old generations.

It collects short-lived young objects more frequently and long-lived old objects less frequently.


Question 21: Why does Generational Shenandoah improve performance?

Answer

Most objects die shortly after creation.

Collecting young objects separately can reduce:

  • Marking work
  • Heap scanning
  • Evacuation volume
  • Barrier overhead
  • CPU consumption

It can also improve throughput and allocation-spike resilience.


Question 22: In which JDK did Generational Shenandoah become a product feature?

Answer

It became a product feature in JDK 25 through JEP 521.


Question 23: When did Generational Shenandoah become the default mode?

Answer

Generational Shenandoah became the default Shenandoah mode in JDK 26 through JEP 535.


Question 24: What is the difference between Shenandoah and G1GC?

Answer

Both are region-based and perform concurrent marking.

The major difference is object movement:

  • G1 primarily evacuates objects during Stop-The-World pauses.
  • Shenandoah evacuates objects concurrently.

This generally gives Shenandoah lower pauses but may add barrier and CPU overhead.


Question 25: What is the difference between Shenandoah and ZGC?

Answer

Both are low-latency concurrent compacting collectors.

Shenandoah traditionally uses forwarding-pointer and load-reference-barrier techniques, along with an explicit concurrent update-reference phase.

ZGC uses colored-pointer metadata, load barriers, forwarding information, and lazy reference repair.


Question 26: Is Shenandoah always better than G1?

Answer

No.

Shenandoah may provide shorter pauses, but G1 may provide:

  • Better throughput
  • Lower barrier overhead
  • Simpler operational behavior
  • Better results for general-purpose workloads

The decision should be based on production benchmarks.


Question 27: When should Shenandoah be used?

Answer

Consider Shenandoah when:

  • p99 latency is critical.
  • G1 pauses violate service-level objectives.
  • The application uses a medium or large heap.
  • Enough CPU is available for concurrent GC work.
  • Sufficient heap headroom is available.
  • The JDK distribution supports Shenandoah.

Question 28: When should Shenandoah be avoided?

Answer

It may not be ideal when:

  • Maximum throughput is the main goal.
  • CPU is severely constrained.
  • The application has a tiny heap.
  • Heap headroom is limited.
  • G1 already meets latency requirements.
  • The chosen JDK distribution does not include or support it.

Question 29: What should be monitored besides pause time?

Answer

Monitor:

  • Pacing
  • Degenerated GC
  • Full GC
  • Allocation rate
  • Live-data-set growth
  • GC CPU overhead
  • Container CPU throttling
  • Post-GC heap occupancy
  • Safepoint time
  • Application p99 latency

Short pauses alone do not prove the collector is healthy.


Question 30: What is the most important Shenandoah tuning principle?

Answer

Provide adequate heap and CPU headroom.

A concurrent collector must complete marking, evacuation, and reference updating before the application consumes all available memory.


Real-Time Production Scenario 1: Degenerated GC

Problem

A Spring Boot API uses:

-Xmx8g
-XX:+UseShenandoahGC

GC logs show:

Pause Degenerated GC
Pause Degenerated GC
Pause Full GC

Heap occupancy remains above 90%.

Likely causes

  • Live-data set close to 8 GB
  • Unbounded cache
  • Allocation rate too high
  • Insufficient CPU
  • Concurrent cycle starting too late

Investigation

  1. Check post-GC heap occupancy.
  2. Measure allocation rate.
  3. Capture a heap dump.
  4. Review cache size and retention.
  5. Check pod CPU throttling.
  6. Inspect Shenandoah pacing events.
  7. Compare cycle duration with time to heap exhaustion.

Resolution

  • Fix retained-object growth.
  • Increase heap only when live data is legitimate.
  • Add cache eviction.
  • Increase CPU allocation.
  • Reduce temporary allocations.
  • Stream large payloads.
  • Test Generational Shenandoah on an appropriate JDK.

Real-Time Production Scenario 2: Low GC Pauses but Slow Requests

Symptoms

  • GC pauses remain under 5 milliseconds.
  • Application p99 latency reaches 800 milliseconds.
  • CPU remains near 100%.
  • No Full GC occurs.

Possible explanation

Shenandoah is running expensive concurrent work alongside application threads.

The application may suffer from:

  • CPU contention
  • Pacing
  • Mutator assistance
  • Slow concurrent evacuation
  • Container throttling

Resolution

Measure:

  • GC CPU usage
  • Application CPU usage
  • Pacing delay
  • Thread scheduling
  • Concurrent phase durations
  • Pod CPU throttling

Low pause time does not mean zero collector overhead.


Real-Time Production Scenario 3: Too Many Threads

Problem

A service uses thousands of platform threads.

Initial Mark and Final Update References pauses become longer.

Explanation

Each platform thread contributes stack and root-processing work.

Shenandoah's pauses are influenced strongly by root-set size.

Possible actions

  • Reduce unnecessary platform threads.
  • Use bounded thread pools.
  • Consider virtual threads for suitable I/O workloads.
  • Remove excessive ThreadLocal state.
  • Examine root-processing details in GC logs.
  • Review agents and JNI integrations.

Real-Time Production Scenario 4: Large File Allocation

Bad implementation

public void processFile(MultipartFile file) throws IOException {

    byte[] data = file.getBytes();

    process(data);
}

For a 1 GB file, this creates a very large heap object and may create additional copies.

Risks

  • Heap headroom reduction
  • Increased allocation pressure
  • Pacing
  • Degenerated GC
  • OutOfMemoryError

Better implementation

public void processFile(MultipartFile file) throws IOException {

    try (InputStream input = file.getInputStream();
         BufferedInputStream buffered = new BufferedInputStream(input)) {

        byte[] buffer = new byte[64 * 1024];
        int count;

        while ((count = buffered.read(buffer)) != -1) {
            processChunk(buffer, count);
        }
    }
}

Streaming reduces peak heap pressure.


Real-Time Production Scenario 5: G1 to Shenandoah Migration

Existing G1 configuration

-Xms8g
-Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

Metrics:

p50 response time:      40 ms
p95 response time:     110 ms
p99 response time:     850 ms
Maximum mixed pause:   700 ms
CPU utilization:        55%

Shenandoah test

-Xms8g
-Xmx8g
-XX:+UseShenandoahGC

Compare:

  • p50 latency
  • p95 latency
  • p99 latency
  • p99.9 latency
  • Requests per second
  • CPU consumption
  • Heap headroom
  • Degenerated GC
  • Full GC
  • Pacing
  • Infrastructure cost

Do not migrate only because Shenandoah has shorter average pauses.

The goal is improved application-level performance.


Troubleshooting Flow

flowchart TD
    Problem["Latency or Memory Problem"] --> Logs["Analyze Shenandoah Logs"]

    Logs --> Degenerated{"Degenerated GC?"}
    Degenerated -->|Yes| Pressure["Check Allocation Pressure"]
    Pressure --> Heap["Check Heap Headroom"]
    Pressure --> CPU["Check CPU Throttling"]
    Pressure --> Leak["Check Live-Set Growth"]

    Degenerated -->|No| Full{"Full GC?"}
    Full -->|Yes| Dump["Capture Heap Dump"]
    Dump --> Retained["Analyze Retained Objects"]

    Full -->|No| Pauses{"Long Pauses?"}
    Pauses -->|Yes| Roots["Analyze Root-Set Processing"]
    Roots --> Threads["Check Thread Count"]
    Roots --> JNI["Check JNI and Agents"]

    Pauses -->|No| Overhead["Check Concurrent GC CPU and Pacing"]

Best Practices

  • Use a recent supported OpenJDK build.
  • Confirm that your JDK vendor includes Shenandoah.
  • Enable detailed GC and safepoint logs.
  • Provide adequate heap headroom.
  • Provide adequate CPU for concurrent phases.
  • Monitor Degenerated and Full GC counts.
  • Monitor pacing delays.
  • Use bounded caches.
  • Stream large files.
  • Avoid unnecessary temporary objects.
  • Keep thread counts under control.
  • Include native memory when sizing containers.
  • Test Generational Shenandoah on JDK 25 or later.
  • Benchmark against G1 and ZGC.
  • Change one JVM option at a time.
  • Fix memory leaks before collector tuning.

Common Mistakes

❌ Assuming Shenandoah has no Stop-The-World pauses.

❌ Looking only at pause duration.

❌ Ignoring pacing and Degenerated GC.

❌ Setting Xmx equal to the container limit.

❌ Running a concurrent collector with insufficient CPU.

❌ Increasing heap size to hide a memory leak.

❌ Using diagnostic heuristics in production.

❌ Assuming every JDK distribution includes Shenandoah.

❌ Ignoring root-set size.

❌ Loading entire large files into memory.

❌ Assuming Shenandoah always outperforms G1.

❌ Using outdated generational flags without checking the JDK version.


Senior Interview Tips

Senior interviewers may ask:

  • How does Shenandoah perform concurrent compaction?
  • What is the purpose of the Brooks pointer?
  • How does a load-reference barrier work?
  • How can application threads access objects during evacuation?
  • What is the Collection Set?
  • Why does Shenandoah require Concurrent Update References?
  • What is SATB?
  • What is pacing?
  • What causes Degenerated GC?
  • How is Degenerated GC different from Full GC?
  • Why does CPU throttling hurt Shenandoah?
  • Why can large root sets increase pauses?
  • How does Generational Shenandoah improve throughput?
  • When should G1 be preferred?
  • How would you diagnose low pauses but poor p99 latency?

A strong answer should connect collector internals to production symptoms and operational trade-offs.


Quick Revision Table

Concept Interview answer
Shenandoah Concurrent, region-based, low-pause compacting collector
Concurrent Mark Finds reachable objects while application threads run
Collection Set Regions selected for evacuation
Concurrent Evacuation Moves live objects while the application runs
Brooks Pointer Forwarding indirection to an object's current location
Load-Reference Barrier Resolves references during concurrent relocation
Update References Replaces stale addresses after evacuation
SATB Snapshot-based concurrent-marking strategy
Pacing Slows allocations so GC can catch up
Degenerated GC Completes a failed concurrent cycle Stop-The-World
Full GC Last-resort heap-wide collection
Generational Shenandoah Separates young and old objects
JDK 25 Generational mode became a product feature
JDK 26 Generational mode became the default
Main tuning need Adequate heap and CPU headroom

Summary

In this article, we covered:

  • Shenandoah GC architecture
  • Region-based heap management
  • Concurrent marking
  • Concurrent evacuation
  • Concurrent reference updating
  • Concurrent compaction
  • Brooks forwarding pointers
  • Load-reference barriers
  • Mutator assistance
  • SATB marking
  • Collection Sets
  • Pacing
  • Degenerated GC
  • Full GC
  • Shenandoah modes and heuristics
  • Generational Shenandoah
  • JDK version differences
  • Heap and CPU sizing
  • Kubernetes considerations
  • GC logging
  • Production troubleshooting
  • Shenandoah versus G1GC
  • Shenandoah versus ZGC
  • Real interview questions
  • Best practices
  • Common mistakes

In the next article, we will explore GC Tuning, including:

  • Heap-sizing strategy
  • Xms and Xmx
  • Live-data-set measurement
  • Allocation and promotion rates
  • Pause-time and throughput goals
  • Container-aware heap sizing
  • G1, ZGC, and Shenandoah tuning
  • Memory-leak investigation
  • Java Flight Recorder
  • Heap-dump analysis
  • Production tuning methodology