Backend Architecture

Modern Java Garbage Collection: Deep Dive into G1, Generational ZGC, and Shenandoah

Explore modern Java garbage collection with an in-depth look at G1, Generational ZGC, and Shenandoah. Learn their architecture, trade-offs, and how to choose the right low-latency collector for your JVM workload.

Modern Java Garbage Collection: Deep Dive into G1, Generational ZGC, and Shenandoah

In high-throughput distributed microservices, tail latency (P99/P99.9) is king. A backend service can maintain an average response time of 5ms, but if a periodic 200ms Stop-The-World (STW) garbage collection pause strikes, downstream API gateways timeout, Kafka consumers risk heartbeat failures triggering partition rebalances, and customer-facing SLAs fail.

Java’s garbage collection has seen radical innovations across LTS releases. The days of accepting hundreds of milliseconds of application freezes are over. Today, JVM performance engineers can choose between high-throughput regional engines and fully concurrent, sub-millisecond low-latency collectors.

This deep dive deconstructs the inner mechanics, memory barriers, throughput costs, and trade-offs of G1 GC, Generational ZGC, and Shenandoah GC.


1. G1 GC: The Balanced Generational Workhorse

Garbage-First (G1) has been the default HotSpot collector since Java 9. Designed to replace CMS (Concurrent Mark Sweep), G1 strikes an engineering balance between raw CPU throughput and bounded pause times.

Internal Architecture & Region Grid

  • Region-Based Memory: Instead of allocating contiguous physical space for Young and Old generations, G1 subdivides the heap into 2,048 logical, non-contiguous regions ranging from 1MB to 32MB (always a power of two, determined dynamically by -Xmx).
  • Dynamic Generation Roles: Each region dynamically assumes a role: Eden, Survivor, Old, or Humongous (objects larger than 50% of a region size).
  • Collection Set (CSet): G1 continuously tracks live object density and reclaimable garbage per region. During collection, it prioritizes regions with the highest proportion of reclaimable space ("Garbage-First") to maximize freed memory within the target pause window.
+-----------------------------------------------------------+
[Eden] [Old] [Eden] [Humongous] [Survivor]
[Old] [Survivor][Eden] [Old] [Old]
[Eden] [Old] [Survivor] [Free] [Eden]
+-----------------------------------------------------------+ Logical 1MB–32MB Independent Region Grid

The Evacuation Phase & STW Mechanics

G1 marks objects concurrently during normal application execution using Snapshot-At-The-Beginning (SATB) write barriers. However, the actual compaction and evacuation phase is strictly Stop-The-World (STW):

  1. Application threads halt at an aligned JVM safepoint.
  2. Live objects from the chosen CSet regions copy into fresh, empty survivor regions.
  3. Survivor objects age and promote to Old regions as needed.
  4. Old regions in the CSet are reclaimed in their entirety.
Because evacuation moves memory and updates all incoming object references via card tables, doing so during an STW pause simplifies consistency and eliminates memory barrier overhead during active mutator runtime.
  • Target Pause Control: Configured via -XX:MaxGCPauseMillis=200 (default). G1 dynamically resizes Young generation regions to satisfy this target based on historical decay metrics.
  • Limitation: Under sudden high-throughput allocation bursts, G1 cannot evacuate fast enough within the target window, resulting in extended STW pauses or cascading into an expensive single-threaded Full GC fallback.

2. Generational ZGC: Sub-Millisecond Pauses at Terabyte Scale

Integrated as production-ready in Java 15 and enhanced with generational capabilities via JEP 439, ZGC is an ultra-low latency collector engineered to scale predictably from tens of megabytes to 16 Terabytes.

Core Mechanics: Colored Pointers & Load Barriers

ZGC executes marking, evacuation, and reference remapping concurrently alongside running application threads.

1. Colored Pointers

ZGC utilizes reference coloring by embedding metadata directly into the unused upper bits of 64-bit virtual memory pointers:

+-------------------+-------------+-----------------------------------------------+
16 bits (unused)4 metadata44 bits (Object Virtual Memory Address)
bitsSupports up to 16 Terabytes of address space
+-------------------+-------------+-----------------------------------------------+ | +-------------+-------------+ | Marked0 | Marked1 | Remapped | Finalizable |

Because metadata resides directly in the pointer reference, the runtime determines an object's GC state without dereferencing the pointer or reading object header cache lines.

2. JIT Load Barriers (Self-Healing Pointers)

When an application thread dereferences an object field (e.g., obj.field), a JIT-compiled Load Barrier validates the colored bits of the reference:

  • If the pointer is marked Remapped, execution proceeds immediately at native CPU speed (cost: ~1–2 instructions).
  • If the pointer points to an object in an active evacuation page, the load barrier intercepts the call:
1. Inspects the thread-safe forwarding table to verify if the object was already relocated. 2. If unevacuated, the application thread relocates the object itself into fresh memory. 3. Updates the reference pointer in place and colors it Remapped.

Subsequent reads directly hit the updated address—this is the self-healing mechanism.

Application Thread -> Read reference obj.field
                            |
                     [Load Barrier Test]
                      /              \
         Pointer is 'Remapped'?     Pointer 'Not Remapped' (In Evacuation Set)
                 |                                |
             Fast Path                    Slow Path (Self-Heal):
         (Execute instantly)              1. Relocate object / read forward table
                                          2. Update pointer in place
                                          3. Mark pointer 'Remapped'

Generational ZGC Performance Profile

Generational ZGC segregates memory into Young and Old generations while preserving concurrent relocation. Because most objects die young (the weak generational hypothesis), collecting the young generation frequently reclaims memory rapidly with negligible CPU overhead, preventing allocation stalls under intense traffic.

  • Typical Pauses: Less than 1ms (often sub-250 microseconds), independent of whether the heap is 16GB or 16TB.

3. Shenandoah GC: Concurrent Compaction via Forwarding Pointers

Developed by Red Hat and integrated into OpenJDK, Shenandoah is an ultra-low latency garbage collector designed to eliminate compaction-induced pause spikes without requiring multi-terabyte infrastructure.

Core Mechanics: Forwarding Pointers & Load-Reference Barriers

Like G1, Shenandoah structures memory into discrete regions. Unlike G1, it performs the expensive object evacuation phase concurrently.

1. Forwarding Pointers (Brooks Pointers)

To allow concurrent relocation while application threads actively mutate memory:

  • Every object maintains an internal forwarding pointer in its header.
  • In steady-state, the forwarding pointer points directly to the object itself.
  • When Shenandoah evacuates an object:
1. A background GC worker allocates space in a target region and copies the object payload. 2. It performs an atomic Compare-And-Swap (CAS) to redirect the old object's forwarding pointer to the new copy. 3. If an application thread accesses the old copy during the race window, the barrier inspects the pointer and resolves the call to the new location.
Old Object Memory:                          New Object Memory:
+------------------------+                  +------------------------+
| Forwarding Pointer ----|-------------->   | Forwarding Pointer (self)
+------------------------+                  +------------------------+
| Object Fields / Payload|                  | Object Fields / Payload|
+------------------------+                  +------------------------+

2. Load-Reference Barriers (LRB)

Modern Shenandoah uses Load-Reference Barriers to intercept heap loads. Whenever an application thread dereferences an object, the barrier checks the forwarding pointer, ensuring the application always mutates the canonical, up-to-date copy during active evacuation.

  • Typical Pauses: Sub-10ms, strictly bounded by minor root-scanning safepoints (Init Mark, Final Mark, Init Evac).

Direct Architecture Comparison

Feature / MetricG1 GCGenerational ZGCShenandoah GC
Primary Design GoalBalanced throughput & bounded pausesUltra-low latency across massive heapsLow latency via concurrent compaction
P99 Pause Target10ms – 200ms (Soft limit)< 1ms (Deterministic)< 10ms (Deterministic)
Marking PhaseConcurrentConcurrentConcurrent
Evacuation / CompactionStop-The-World (STW)ConcurrentConcurrent
Core MechanismCard tables & SATB write barriersColored pointers & Load barriersForwarding pointers & LRBs
Generational SupportFully GenerationalFully GenerationalEvolving / Generation-aware modes
Throughput OverheadLowest (~1–3% CPU)Low (~3–5% CPU)Moderate (~5–8% CPU)
Optimal Heap Size4GB – 32GB16GB – 16TB+4GB – 64GB+

Production Recommendations & JVM Flags

Standard Enterprise Microservice (G1 GC)

Best for raw batch throughput where 50ms–100ms pause spikes will not violate SLAs:
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=100 \
     -XX:InitiatingHeapOccupancyPercent=45 \
     -Xms16g -Xmx16g \
     -XX:+AlwaysPreTouch \
     -jar service.jar

Ultra-Low Latency & Financial APIs (Generational ZGC)

Best for strict sub-millisecond P99/P99.9 latency SLAs on modern Java builds:
java -XX:+UseZGC \
     -XX:+ZGenerational \
     -Xms32g -Xmx32g \
     -XX:+AlwaysPreTouch \
     -jar service.jar

Containerized Medium Heaps (Shenandoah GC)

Best for sub-10ms response constraints on medium heaps (4GB–32GB) where low pause jitter is required:
java -XX:+UseShenandoahGC \
     -XX:ShenandoahGCMode=satb \
     -Xms16g -Xmx16g \
     -XX:+AlwaysPreTouch \
     -jar service.jar

The Kubernetes CPU Limit Trap: CFS Throttling & Allocation Stalls

Deploying concurrent garbage collectors (Generational ZGC or Shenandoah) inside containerized cloud environments introduces an operational trap: Linux Completely Fair Scheduler (CFS) throttling.

How STW Collectors Behave Under CFS

Under G1, when an evacuation pause begins, mutator threads stop executing. The JVM consumes available CPU quotas exclusively to evacuate objects as fast as possible before resuming application traffic.

How Concurrent Collectors Trigger Throttling

Under ZGC or Shenandoah, GC worker threads run concurrently on background cores while HTTP request-handling threads process inbound payloads.

Consider this common Kubernetes resource specification:

resources:
  requests:
    cpu: "4"
    memory: "16Gi"
  limits:
    cpu: "4"        # <-- Latency Risk
    memory: "16Gi"

During heavy ingress traffic, your application mutators and JVM GC background threads burn compute cycles simultaneously. The Linux CFS tracks accumulated CPU runtime across the container. Once the aggregate execution hits the millisecond quota within the default period (cpu.cfs_period_us, typically 100ms), the kernel throttles the entire container.

[HTTP Worker Threads (4 cores)] + [Concurrent GC Threads (2 cores)]
                              |
                     Surpasses 4 CPU limit
                              |
                    CFS Quota Exhausted!
                              |
              Kernel Throttles Entire Container
                              |
       Mutators continue allocating memory while paused
                              |
                  ALLOCATION STALL OCCURS
               (Latency spikes to 2–5 seconds)

The Failure Cascade (Allocation Stalls)

When threads are throttled, application threads continue allocating memory faster than the throttled GC workers can reclaim pages. Once free regions are exhausted, the JVM enters a synchronous allocation stall:

  1. Application threads attempting to allocate objects are placed on wait queues.
  2. Latency jumps from less than 1ms directly to multiple seconds.
  3. Health check endpoints fail, prompting Kubernetes liveness probes to kill the pod.

Production Mitigation Rules

  • Over-provision CPU headroom: Allocate concurrent collectors at least 20–25% more CPU capacity than a baseline G1 deployment to absorb concurrent background sweep cycles.
  • Avoid tight CPU limits: Match CPU requests to actual usage and consider omitting CPU limits or leveraging the Kubernetes Guaranteed QoS tier with static CPU core pinning for latency-critical workloads.
  • Pre-touch memory pages: Always pair -Xms equal to -Xmx with -XX:+AlwaysPreTouch to force page-table population during JVM startup, preventing runtime page fault latency.
#java#garbage collection#g1#zgc#shenandoah#spring boot#JVM#Performance Engineering#Kubernetes#DevOps
Shashank Dubey

Written by Shashank Dubey

Software Engineer specializing in Java Spring Boot, Node.js, React, distributed microservices, and enterprise cloud systems at National Informatics Centre (NIC), Government of India.

View Full Bio & Credentials →


Related In-Depth Engineering Guides