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:
- Initial Mark
- Concurrent Mark
- Final Mark
- Concurrent Cleanup
- Concurrent Evacuation
- Initial Update References
- Concurrent Update References
- Final Update References
- 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:
adaptivestaticcompactaggressive
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
- Check post-GC heap occupancy.
- Measure allocation rate.
- Capture a heap dump.
- Review cache size and retention.
- Check pod CPU throttling.
- Inspect Shenandoah pacing events.
- 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
XmsandXmx- 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