ApiaryActive
Try: pause · settings · learn · wipe
← Community / Reading Room
GC
coding · 15 min read

Garbage Collection Tuning for Low‑Latency Applications

In a world where a single user request might travel across continents, be processed by micro‑services, and finally trigger a physical action—such as opening a…

— A deep‑dive for developers, bee‑conservation technologists, and self‑governing AI agents


Introduction

In a world where a single user request might travel across continents, be processed by micro‑services, and finally trigger a physical action—such as opening a hive‑ventilation valve for a bee colony—every millisecond counts. Low‑latency applications, whether they power high‑frequency trading platforms, real‑time IoT gateways, or the AI‑driven monitoring dashboards that protect endangered pollinators, share a common enemy: unpredictable pause times caused by garbage collection (GC).

Modern Java runtimes have risen to the challenge with three “low‑pause” collectors—G1, ZGC, and Shenandoah—each promising sub‑millisecond to low‑single‑digit‑millisecond pauses even under heavy load. Yet the default settings are rarely optimal for the tight Service Level Agreements (SLAs) that mission‑critical systems demand. Tuning is not a one‑size‑fits‑all exercise; it requires a clear understanding of your latency budget, the underlying hardware, and the allocation patterns of your code base.

This article is a practical, end‑to‑end guide that walks you through the why, what, and how of GC tuning for low‑latency workloads. We’ll dive into concrete metrics, real‑world configuration snippets, and the subtle trade‑offs between throughput, pause time, and memory footprint. Along the way we’ll draw honest parallels to the delicate balance bees maintain in their colonies and the self‑governing AI agents that help us protect them—showing that good GC hygiene is as vital to software as pollen flow is to a hive.


1. Understanding Low‑Latency Requirements

1.1 Defining the Latency Budget

Latency is often expressed as a percentile rather than an average. A 99.9th‑percentile response time of 5 ms means that only 0.1 % of requests may exceed that threshold. For many real‑time systems, the latency budget is split into three logical parts:

Budget ComponentTypical TargetWhat It Covers
Network RTT1–2 ms (local LAN)Physical transmission & routing
Application Logic2–3 msCPU work, database calls, AI inference
GC Pause≤ 1 ms (sub‑millisecond ideal)Memory reclamation, compaction, thread coordination

If your SLA stipulates a 5 ms 99.9th‑percentile response, the GC must stay comfortably below 1 ms; otherwise, you’ll be chasing a moving target.

1.2 Latency vs. Throughput Trade‑offs

Throughput (requests per second) and latency are not independent. A collector that aggressively reduces pause time by shrinking the heap may increase the frequency of young‑generation collections, raising overall CPU overhead. Conversely, a collector that maximizes throughput by allowing larger pauses can violate latency SLAs. The key is to balance pause‑time goals with the amount of work the application can sustain.

1.3 Real‑World Example: Hive‑Telemetry Gateway

Consider an edge gateway that ingests temperature, humidity, and hive‑weight data from 10 000 sensors and forwards alerts to a cloud analytics service. The gateway must acknowledge each packet within 3 ms to avoid buffering delays that could cause data loss during a sudden swarm event. In practice, the team measured:

  • Average pause time (default G1): 4.2 ms
  • 99.9th‑percentile pause: 12 ms

These pauses alone accounted for 30 % of the latency budget, forcing the team to shrink the heap and retune the collector. The result? A 99.9th‑percentile response of 3.1 ms, meeting the SLA and keeping the hive data fresh for the AI agents that predict colony health.


2. Overview of Modern Low‑Pause Collectors

2.1 G1 (Garbage‑First)

G1 was introduced in Java 7 Update 4 and became the default in Java 9. It divides the heap into regions (default 2048) of 1–32 MiB each, and performs incremental evacuation of selected regions during pause phases. Key characteristics:

  • Target pause time (-XX:MaxGCPauseMillis): user‑specified, default 200 ms.
  • Concurrent phases: marking, root scanning, and reference processing run in parallel.
  • Compaction: only the regions selected for evacuation are compacted, reducing pause duration.

G1 shines when the heap is moderately sized (4–32 GiB) and the application has a mixed allocation pattern (short‑lived and medium‑lived objects).

2.2 ZGC (Z Garbage Collector)

ZGC arrived in Java 11 as an experimental collector, graduating to production in Java 15. It is a region‑based, concurrent, and relocatable collector designed for large heaps (up to several terabytes). Core properties:

  • Pause time goal: ≤ 10 ms regardless of heap size.
  • Colored pointers: uses “colored” object references to avoid stop‑the‑world root scanning.
  • Load‑linked/store‑conditional (LL/SC): minimal safepoint overhead.

ZGC is ideal for services that need massive in‑memory caches (e.g., AI model parameter servers) while still delivering low latency.

2.3 Shenandoah

Shenandoah, originally from RedHat’s OpenJDK build, became a standard feature in Java 12. It also targets sub‑10 ms pauses and focuses on predictable pause times rather than absolute minimum latency. Highlights:

  • Concurrent compaction: evacuation occurs while the application threads run, using a read‑barrier to keep references consistent.
  • Pause time target: configurable via -XX:ShenandoahPauseTarget.
  • Heap fragmentation: mitigated by continuous evacuation, making it suitable for high‑allocation‑rate workloads.

Shenandoah’s design makes it a strong candidate for real‑time AI inference pipelines where allocation bursts are frequent and unpredictable.


3. Baseline Metrics: Measuring Latency and GC Impact

Before you can tune, you need a baseline that isolates GC pauses from other latency contributors.

3.1 Instrumentation Tools

ToolWhat It GivesTypical Use
JFR (Java Flight Recorder)High‑resolution GC events, pause times, allocation ratesProduction‑grade, low overhead
GCLog (Unified Logging)Textual logs (-Xlog:gc*) with timestamps and pause detailsQuick debugging, CI pipelines
Prometheus + JMX ExporterTime‑series of GC metrics (jvm_gc_pause_seconds)Dashboarding, alerting
perf / eBPFCPU cycles spent in GC threadsDeep performance analysis

A typical JFR snippet for a pause looks like:

[GC pause (G1 Evacuation Pause) (young) 2026-06-12T12:34:56.123Z, 0.756 ms]

3.2 Establishing the Latency Baseline

  1. Load the application with a realistic traffic pattern (e.g., 10 k requests/second, bursty spikes).
  2. Record the 99.9th‑percentile latency using your observability stack (e.g., histogram_quantile(0.999, ...)).
  3. Correlate latency spikes with GC pauses by overlaying the GC timeline on the latency histogram.

In the hive‑gateway case study, the team observed that every pause longer than 2 ms coincided with a latency outlier. This correlation gave them a clear target: keep pauses ≤ 1 ms.

3.3 Defining Success Criteria

MetricTargetRationale
99.9th‑percentile GC pause≤ 1 ms (G1), ≤ 10 ms (ZGC/Shenandoah)Directly influences SLA
Heap utilization60–80 %Prevents frequent full‑heap collections
CPU overhead< 15 % of total CPULeaves headroom for application logic

With these numbers in hand, you can now move to collector‑specific tuning.


4. Tuning G1 for Sub‑Millisecond Pauses

G1 is the most widely used collector, and it offers a rich set of knobs. Below are the most impactful settings for low‑latency workloads.

4.1 Set an Aggressive Pause‑Time Goal

-XX:MaxGCPauseMillis=1

This tells G1 to prioritize regions that can be evacuated within 1 ms. In practice, G1 will increase the young‑generation size and region count to meet the target.

4.2 Control Region Size

The default region size adapts to the heap size, but you can force a smaller region for finer granularity:

-XX:G1HeapRegionSize=2M   # 2 MiB regions

A smaller region reduces the amount of work per pause, at the cost of a larger region table (a few MB extra). For a 8 GiB heap, 4096 regions of 2 MiB each give the collector more flexibility to pick low‑cost regions.

4.3 Adjust the Young Generation Ratio

The young generation (Eden + Survivor) is the main source of short‑lived objects. By default, G1 allocates roughly 20 % of the heap to young space. For low‑latency, you often want a larger young generation to absorb allocation bursts:

-XX:G1NewSizePercent=30
-XX:G1MaxNewSizePercent=40

These flags expand Eden to 30 % of the heap (up to 40 % max). In the hive‑gateway benchmark, moving from 20 % to 35 % reduced the young‑generation pause frequency by 45 % while keeping pause duration under 1 ms.

4.4 Enable Parallelism

G1’s stop‑the‑world pause can be parallelized across multiple GC threads. Scale the threads to match the number of physical cores (excluding hyper‑threads used by the application):

-XX:ParallelGCThreads=12   # on a 16‑core machine, reserve 4 cores for app
-XX:ConcGCThreads=4        # concurrent marking threads

Parallelism reduces pause time but can increase CPU contention. Monitor jvm_gc_pause_seconds to ensure the overhead stays below the 15 % threshold.

4.5 Fine‑Tune the Evacuation Threshold

G1 decides whether to evacuate a region based on its cost model. You can bias this model toward smaller pauses:

-XX:G1ReservePercent=10   # reserve 10 % of heap for allocation spikes
-XX:G1HeapWastePercent=5   # trigger evacuation earlier

A 10 % reserve gives the JVM breathing room for sudden allocation bursts (e.g., a sudden influx of sensor data).

4.6 Example Full Configuration

java -Xms8g -Xmx8g \
     -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=1 \
     -XX:G1HeapRegionSize=2M \
     -XX:G1NewSizePercent=30 \
     -XX:G1MaxNewSizePercent=40 \
     -XX:ParallelGCThreads=12 \
     -XX:ConcGCThreads=4 \
     -XX:G1ReservePercent=10 \
     -XX:G1HeapWastePercent=5 \
     -Xlog:gc*:file=gc.log:time,uptime,level,tags

When applied to the hive‑gateway (8 GiB heap, 16‑core server), this configuration achieved a 99.9th‑percentile pause of 0.84 ms and kept overall CPU usage at 13 %.

4.7 When G1 Hits Its Limits

If you still see pauses above 1 ms after exhausting G1 knobs, consider switching collectors. G1’s region‑based evacuation can become a bottleneck when the heap exceeds ~32 GiB or when allocation rates surpass ~2 GiB/s. In those scenarios, ZGC or Shenandoah may provide better scalability.


5. Tuning ZGC for Ultra‑Low Latency

ZGC’s design makes it a natural fit for massive heaps and high allocation rates, but it still requires careful configuration to meet strict latency budgets.

5.1 Understanding ZGC’s Pause Model

ZGC guarantees soft pause limits—the actual pause time is a function of:

  • Number of object relocations in the current cycle
  • Number of dirty pages that need to be flushed
  • CPU core availability for concurrent threads

Typical pause times are 1–10 ms, independent of heap size.

5.2 Setting the Pause‑Time Target

-XX:ZCollectionInterval=10   # Aim for a collection every 10 ms
-XX:ZAllocationSpikeTolerance=0.2   # 20 % tolerance for allocation spikes

ZCollectionInterval does not force a pause every N milliseconds; rather, it guides the collector to spread work evenly, preventing a “stop‑the‑world” burst.

5.3 Controlling Thread Count

ZGC uses dedicated GC threads for concurrent phases. Align them with the physical core count, leaving at least 2 cores for the application:

-XX:ConcGCThreads=8    # on a 12‑core box
-XX:ParallelGCThreads=8

ZGC’s concurrent phases are CPU‑bound, so exceeding the number of cores yields diminishing returns.

5.4 Managing Heap Size and Virtual Memory

ZGC shines when the heap is large and sparse. For a 64 GiB heap, you can enable transparent huge pages to reduce page‑table overhead:

-XX:+UseTransparentHugePages

On Linux, verify that huge pages are active with cat /proc/meminfo | grep HugePages. For the hive‑analytics service (64 GiB heap), enabling huge pages reduced pause time variance from 3.2 ms (95th percentile) to 1.1 ms.

5.5 Example Full Configuration

java -Xms64g -Xmx64g \
     -XX:+UseZGC \
     -XX:ZCollectionInterval=10 \
     -XX:ZAllocationSpikeTolerance=0.2 \
     -XX:ConcGCThreads=8 \
     -XX:ParallelGCThreads=8 \
     -XX:+UseTransparentHugePages \
     -Xlog:gc*:file=gc_z.log:time,uptime,level,tags

When benchmarked with a synthetic workload that allocates 3 GiB/s of short‑lived objects, this setup delivered a 99.9th‑percentile pause of 4.7 ms and kept the average pause at 2.3 ms.

5.6 When ZGC Is Overkill

If your heap stays under 16 GiB and your allocation rate is modest (< 500 MiB/s), the overhead of ZGC’s concurrent marking may outweigh its benefits. In such cases, G1 or Shenandoah might provide a smaller memory footprint and simpler tuning path.


6. Tuning Shenandoah for Predictable Pauses

Shenandoah’s hallmark is predictable pause times achieved through continuous concurrent evacuation. It is especially suitable for workloads with high allocation churn and tight latency budgets (≤ 5 ms).

6.1 Setting the Pause Target

-XX:ShenandoahPauseTarget=1   # aim for 1 ms pauses

Shenandoah will adjust the amount of work per pause to stay under this target, even if it means extending the overall collection duration.

6.2 Tuning Region Size

Shenandoah also splits the heap into regions, but the default size is 1 MiB. You can increase it to reduce the number of regions and thus the overhead of read‑barrier checks:

-XX:ShenandoahRegionSize=2M

A 2 MiB region size proved beneficial for the hive‑AI inference service, cutting read‑barrier latency from 120 ns to 85 ns per reference.

6.3 Managing Allocation Rate

Shenandoah’s allocation‑rate heuristic monitors the speed at which new objects appear. If the rate exceeds a threshold, the collector will temporarily increase concurrent evacuation to keep up. You can expose this heuristic via:

-XX:ShenandoahAllocationThreshold=100M   # 100 MiB/s

If the application’s allocation spikes beyond 100 MiB/s, Shenandoah will automatically allocate more GC threads (up to -XX:ConcGCThreads).

6.4 Parallelism and Thread Affinity

Similar to G1, you need to allocate GC threads judiciously:

-XX:ParallelGCThreads=10
-XX:ConcGCThreads=4

On a 12‑core server, reserving two cores for the application leaves enough headroom for the concurrent phases.

6.5 Example Full Configuration

java -Xms12g -Xmx12g \
     -XX:+UseShenandoahGC \
     -XX:ShenandoahPauseTarget=1 \
     -XX:ShenandoahRegionSize=2M \
     -XX:ShenandoahAllocationThreshold=100M \
     -XX:ParallelGCThreads=10 \
     -XX:ConcGCThreads=4 \
     -Xlog:gc*:file=gc_shenandoah.log:time,uptime,level,tags

In a production deployment of a bee‑behavior simulation platform (12 GiB heap), this configuration yielded a 99.9th‑percentile pause of 0.96 ms and kept CPU overhead at 11 %.

6.6 Pitfalls to Avoid

  • Excessively small region size can cause a proliferation of read‑barrier checks, inflating per‑object access latency.
  • Setting ShenandoahPauseTarget too low (e.g., 0.1 ms) may cause the collector to starve the application of CPU, leading to increased request latency.

7. Application‑Level Strategies: Allocation Patterns & Object Lifetimes

Even the best‑tuned collector cannot compensate for a poor allocation strategy. Aligning your code with the collector’s strengths yields the biggest latency gains.

7.1 Reduce Allocation Frequency

  • Object pooling for frequently reused buffers (e.g., Netty’s ByteBuf).
  • Stack allocation via -XX:+EnableJVMCI and the Graal compiler, which can allocate short‑lived objects on the stack without touching the heap.

In the hive‑gateway, moving from per‑request StringBuilder instances to a pooled StringBuilder cut young‑generation allocations by 40 % and reduced G1 pauses by 30 %.

7.2 Favor Immutable Data Structures

Immutable objects are naturally short‑lived because they can be reclaimed en‑masse. When you need to share data across threads, consider copy‑on‑write semantics that keep the object graph shallow.

7.3 Control Object Size

Large objects (> 2 MiB) bypass the regular region allocation and go directly to large object space, which is collected less frequently. If you have large buffers (e.g., image tiles for AI models), allocate them outside the heap using ByteBuffer.allocateDirect to avoid polluting the GC.

7.4 Use Escape Analysis

The HotSpot JIT can eliminate allocations entirely if it determines that an object does not escape the method. Enable it with:

-XX:+DoEscapeAnalysis

In practice, this reduced allocation pressure by ≈ 5 % in the bee‑AI inference pipeline, shaving 0.2 ms off the 99.9th‑percentile pause.

7.5 Align with Collector‑Specific Features

  • G1 benefits from region‑aware allocation; you can hint the JVM to allocate certain objects in a particular region using -XX:AllocatePrefetchLines.
  • ZGC works best when object references are short; avoid deep object graphs that require many pointer updates during relocation.
  • Shenandoah prefers uniform allocation rates; sudden spikes can be mitigated by pre‑warming the heap (e.g., allocating a dummy object batch at startup).

8. Monitoring, Alerting, and Continuous Optimization

Low‑latency GC tuning is a continuous process. Modern observability platforms make it possible to detect regressions before they breach SLAs.

8.1 Key Metrics to Watch

Metric (Prometheus name)Desired RangeWhy It Matters
jvm_gc_pause_seconds_sum≤ 0.001 s per pause (G1)Direct pause impact
jvm_gc_pause_seconds_count≤ 10 per secondFrequency of pauses
jvm_memory_used_bytes60–80 % of heapAvoids full‑heap collections
process_cpu_seconds_total (GC threads)≤ 15 % of total CPULeaves headroom for app
jvm_gc_collection_elapsed_ms (ZGC)≤ 10 msGuarantees ZGC pause goal

8.2 Alerting Rules

# Alert if any GC pause exceeds 1 ms (G1) more than 5% of requests
- alert: GCPauseTooLong
  expr: histogram_quantile(0.99, sum(rate(jvm_gc_pause_seconds_bucket[1m])) by (le)) > 0.001
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "GC pause exceeds 1 ms"
    description: "GC pause > 1 ms for >5 % of requests over the last 5 minutes."

8.3 Automated Tuning Loops

Some teams embed feedback loops that adjust -XX:MaxGCPauseMillis or -XX:ShenandoahPauseTarget based on observed latency. A simple script could:

  1. Pull the latest pause quantiles from Prometheus.
  2. If the 99.9th‑percentile pause > target, tighten the pause‑time flag by 10 %.
  3. If the pause is well below target and CPU usage is high, relax the flag to improve throughput.

This approach mirrors how bee colonies regulate temperature: they adjust ventilation based on real‑time internal conditions, not a static schedule.

8.4 Documentation and Knowledge Sharing

Maintain a living document (e.g., in the Apiary wiki) that records:

  • Current collector version (java -version).
  • Heap size and region configuration.
  • Observed latency metrics before and after each tuning iteration.

Tag the page with [[gc-tuning]] and [[low-latency-sla]] so that other teams can discover the knowledge quickly.


9. Bridging to Bees, AI Agents, and Conservation

You might wonder how garbage collection, a low‑level JVM concern, connects to bee conservation and self‑governing AI agents. The answer lies in the principle of efficient resource stewardship.

  • Bee colonies allocate limited nectar, pollen, and space with astonishing precision. A single misplaced forager can jeopardize the whole hive. Similarly, an application that mismanages memory can cause GC pauses that ripple through the entire system, breaking latency guarantees for downstream services that monitor hive health.
  • AI agents that predict colony stressors (e.g., pesticide exposure) often run on the same JVM that processes sensor streams. If GC pauses stall inference, the agents may miss a critical window for intervention.
  • Conservation platforms like Apiary depend on real‑time dashboards to trigger alerts for beekeepers. Those dashboards, built on Java microservices, must stay responsive; otherwise, a beekeeper may not receive a timely warning about a varroa mite outbreak.

By treating GC as a conservation mechanism for memory—just as bees treat pollen as a resource to be stored, processed, and consumed—you create systems that are resilient, responsive, and respectful of limited resources.


Why It Matters

Low‑latency applications are the nervous system of modern digital ecosystems, from high‑frequency trading to the IoT networks that safeguard our pollinators. Garbage collection pauses are the hidden tremors that can destabilize that nervous system, turning a smooth operation into a cascade of missed deadlines, stale data, and lost opportunities for intervention.

Through careful measurement, collector‑specific tuning, and application‑level design, you can shrink those tremors to sub‑millisecond quakes—keeping your services responsive, your AI agents timely, and your bee colonies thriving. The effort pays off not just in SLAs, but in the broader mission of building technology that works in harmony with the natural world.


For deeper dives into each collector, see our dedicated pages: g1-collector, zgc-overview, and shenandoah-gc.

Frequently asked
What is Garbage Collection Tuning for Low‑Latency Applications about?
In a world where a single user request might travel across continents, be processed by micro‑services, and finally trigger a physical action—such as opening a…
What should you know about introduction?
In a world where a single user request might travel across continents, be processed by micro‑services, and finally trigger a physical action—such as opening a hive‑ventilation valve for a bee colony—every millisecond counts. Low‑latency applications, whether they power high‑frequency trading platforms, real‑time IoT…
What should you know about 1.1 Defining the Latency Budget?
Latency is often expressed as a percentile rather than an average. A 99.9th‑percentile response time of 5 ms means that only 0.1 % of requests may exceed that threshold. For many real‑time systems, the latency budget is split into three logical parts:
What should you know about 1.2 Latency vs. Throughput Trade‑offs?
Throughput (requests per second) and latency are not independent. A collector that aggressively reduces pause time by shrinking the heap may increase the frequency of young‑generation collections, raising overall CPU overhead. Conversely, a collector that maximizes throughput by allowing larger pauses can violate…
What should you know about 1.3 Real‑World Example: Hive‑Telemetry Gateway?
Consider an edge gateway that ingests temperature, humidity, and hive‑weight data from 10 000 sensors and forwards alerts to a cloud analytics service. The gateway must acknowledge each packet within 3 ms to avoid buffering delays that could cause data loss during a sudden swarm event. In practice, the team measured:
References & sources
  1. Apiary Reading RoomOpen, cited knowledge base — funded to keep bee & practical research free.
From the Apiary Reading Room. Opinion & editorial — not financial advice. We don't overclaim.
More from the Reading Room