A Java API has occasional latency spikes and a dashboard labels them GC pauses. The team enables ZGC and copies a long flag list, but the container begins to restart because heap, native memory, and concurrent collection were never budgeted together.
Use the sequence below as a diagnostic method, not as a promise that one setting fits every host; verify its example paths, privileges, and backup assumptions while working to build a low-latency JVM plan around pause SLOs, live-set size, allocation rate, concurrent-GC headroom, container memory, and repeatable JFR evidence.
Likely failure paths
- The heap maximum leaves too little headroom above the live set for allocation while ZGC performs concurrent work.
- Container memory is sized only for -Xmx and excludes metaspace, code cache, thread stacks, direct buffers, and native libraries.
- The apparent pause is actually CPU throttling, safepoint work, or downstream delay correlated with allocation bursts.
The branches are ordered to protect the strongest evidence around this possibility: the heap maximum leaves too little headroom above the live set for allocation while ZGC performs concurrent work. The observed scope and logs—not a familiar-looking error screen—decide which one applies.
A Java latency incident must separate queueing, downstream saturation, allocation pressure, garbage collection, and transport time; concurrency features change where work waits; they do not create database connections, CPU, or remote capacity; in this guide, the practical goal is to build a low-latency JVM plan around pause SLOs, live-set size, allocation rate, concurrent-GC headroom, container memory, and repeatable JFR evidence.
A controlled investigation
- Define the latency percentile and pause budget, then capture unified GC logs, JFR, process RSS, container throttling, allocation rate, and post-collection live set.
- Compare the event timestamp with application latency and downstream traces instead of assuming correlation proves causation.
- Load test long enough to include promotion, cache warm-up, and a representative live set.
- Confirm the exact JDK release because ZGC defaults and generational behavior change across releases.
The sequence moves from observation toward intervention. Preserve the result of the final check—confirm the exact JDK release because ZGC defaults and generational behavior change across releases—because it provides a useful comparison after the repair.
Start with observable ZGC settings
Run one candidate configuration in a controlled environment and keep unified logs plus JFR with the result.
java -XX:+UseZGC -Xms8g -Xmx8g -XX:+AlwaysPreTouch \
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags \
-XX:StartFlightRecording=filename=/var/log/app/run.jfr,dumponexit=true \
-jar app.jar
Interpretation and safety: Eight gigabytes is illustrative. Reserve container memory for native use and confirm the flags against the deployed JDK; remove AlwaysPreTouch if its startup and resident-memory tradeoff does not fit.
Test one hypothesis without losing the baseline
The opening hypothesis is the heap maximum leaves too little headroom above the live set for allocation while ZGC performs concurrent work. Test it with the least invasive observation available: define the latency percentile and pause budget, then capture unified GC logs, JFR, process RSS, container throttling, allocation rate, and post-collection live set. Do not change configuration until the observation has been saved with a timestamp.
When that evidence is consistent with the hypothesis, stage this repair: start with the documented collector and a maximum heap that holds the live set plus allocation headroom while leaving native memory below the container limit. Otherwise, preserve the current state and advance to the next branch. This keeps rollback simple and prevents a second change from masking the first.
Make the smallest durable change
- Start with the documented collector and a maximum heap that holds the live set plus allocation headroom while leaving native memory below the container limit.
- For strict latency testing, evaluate equal -Xms/-Xmx and AlwaysPreTouch only after measuring startup and resident-memory cost.
- Reduce unnecessary allocation or retained objects when evidence shows the application outruns concurrent collection.
Before applying “Start with the documented collector and a maximum heap that holds the live set plus allocation headroom while leaving native memory below the container limit,” name its rollback point and the evidence that will count as success. Afterward, repeat the original request and specifically check whether you can compare application p99 latency, GC pause distribution, allocation stalls, CPU, and RSS—not only average pause time; a changed symptom at that point is new evidence, not permission to make several more changes at once.
Close the incident with evidence
- Compare application p99 latency, GC pause distribution, allocation stalls, CPU, and RSS—not only average pause time.
- Run a soak that covers the peak live set and confirm the pod is not killed or throttled.
- Repeat with one configuration change at a time and keep the JFR and command line with the result.
One successful refresh is not closure. Keep the incident open until you can also run a soak that covers the peak live set and confirm the pod is not killed or throttled, adjacent paths have not regressed, temporary diagnostics are gone, and another operator can explain what changed.
Prepare a useful escalation if the boundary is outside your control
Capture the JDK build, JVM flags, container limits, request rate, latency percentiles, error rate, thread or JFR evidence, downstream pool occupancy, and the smallest reproducible request; keep tokens, payload data, and internal hostnames out of shared traces; include the result of this first observation: define the latency percentile and pause budget, then capture unified GC logs, JFR, process RSS, container throttling, allocation rate, and post-collection live set.
State what was tested, including the result of “Define the latency percentile and pause budget, then capture unified GC logs, JFR, process RSS, container throttling, allocation rate, and post-collection live set,” and what changed between attempts; evidence tied to that observation is safer and more actionable than granting broad access or sending an unnecessary full database export.
Do not trade visibility for a green screen
- Do not copy obsolete ZGC flags from a different JDK release.
- Do not set heap equal to the container limit or hide a memory leak with a larger heap.
Incident handoff
Primary references
- Oracle HotSpot GC tuning guide — official reference consulted for this guide.
- Oracle Java command reference — official reference consulted for this guide.
Editorial note: The scenario above illustrates how to approach “Start with the documented collector and a maximum heap that holds the live set plus allocation headroom while leaving native memory below the container limit”; it is a documented example, not a claim about a reader’s server, so verify the cited documentation, take the appropriate backup, and follow the real environment’s access and change-control rules.