Without garbage collection monitoring, a memory leak or inefficient GC configuration can silently degrade application performance for hours before anyone notices. A poorly tuned GC causes 100ms+ pauses that destroy p99 latency, trigger cascading service failures, and leave teams troubleshooting symptoms instead of root causes. With GC monitoring, the same memory pressure triggers an alert, surfaces heap behavior patterns, and points to the exact collector setting or allocation hotspot causing the problem.
This guide covers what Java garbage collection monitoring measures, how to interpret GC pause times and throughput metrics, how to choose and tune the right GC algorithm for your workload, and how to instrument GC telemetry into your observability stack.
What Is Java Garbage Collection Monitoring
Java garbage collection monitoring is the practice of tracking JVM memory behavior, GC pause frequency and duration, heap occupancy, and allocation rates to detect memory leaks, tune collector performance, and maintain predictable application latency.
The JVM’s garbage collector automatically reclaims memory from objects that are no longer reachable, preventing manual memory management errors. But GC is not free. Every collection cycle pauses application threads for some duration, and those pauses directly impact user-facing latency. Without monitoring, teams discover GC problems only after users report slow requests or timeout errors.
GC monitoring answers four core questions in production:
- How often is the GC running, and how long do pauses last?
- Is heap usage growing over time, indicating a memory leak?
- Are young generation collections handling normal allocation churn, or is the old generation collecting too frequently?
- Which GC algorithm and tuning parameters fit this workload’s latency and throughput requirements?
Modern APM platforms like infrastructure monitoring tools capture GC metrics as part of full stack observability, correlating GC pauses with request latency spikes, thread pool exhaustion, and downstream service errors.
How Java Garbage Collection Works
Java uses automatic memory management. The JVM allocates objects on the heap, and the garbage collector identifies objects that are no longer reachable by the application, freeing that memory for reuse. This prevents memory leaks caused by manual allocation errors but introduces pause times when the collector runs.
Generational Garbage Collection
Most JVM collectors use a generational model based on the observation that most objects die young. The heap is divided into young generation and old generation regions:
Young generation: Newly allocated objects start here. Most objects become unreachable quickly and are collected during minor GC cycles. The young generation is further divided into Eden space and two survivor spaces.
Old generation: Objects that survive multiple young generation collections are promoted here. Old generation collections (major GC or full GC) are less frequent but take longer because they scan a larger memory region.
Metaspace (Java 8+): Stores class metadata. Metaspace GC is rare and typically only happens when classes are unloaded.
GC Pause Types
Minor GC (young generation collection): Collects only the young generation. Fast, typically under 10ms. Application threads pause during collection.
Major GC (old generation collection): Collects the old generation. Slower than minor GC. Some collectors perform major GC concurrently to reduce pause times.
Full GC: Collects the entire heap including young, old, and sometimes metaspace. Full GC pauses are the longest and most disruptive. Frequent full GCs indicate memory pressure, undersized heap, or a memory leak.
Why GC Pauses Matter
During GC pauses, application threads stop executing. A 200ms pause means every in-flight request waits 200ms longer. For a high-throughput API handling thousands of requests per second, a single long GC pause creates a latency spike visible to users and triggers downstream timeout cascades in microservice architectures.
The goal of GC tuning is not to eliminate GC but to make pauses predictable, keep them under acceptable thresholds (often 50–100ms for web services), and maximize application throughput.
Key GC Metrics to Monitor
Effective GC monitoring tracks these core metrics in production:
GC pause time: Duration of stop-the-world pauses. Track average, p50, p95, and p99 pause times. A p99 pause time over 100ms affects user experience.
GC pause frequency: How often GC runs. Frequent minor GCs are normal for allocation-heavy workloads, but frequent full GCs indicate memory pressure.
Heap usage: Current heap occupancy vs. maximum heap size. If old generation usage grows steadily without dropping, suspect a memory leak.
Allocation rate: How fast the application allocates new objects. High allocation rates stress the young generation and increase GC frequency.
Promotion rate: How many objects survive young generation and get promoted to old generation. Excessive promotion causes premature old generation fills and more full GCs.
GC throughput: Percentage of time spent in application execution vs. GC. Target: 95%+ throughput. Below 90% indicates excessive GC overhead.
Old generation occupancy after full GC: If old generation usage remains high after a full GC, the application may be retaining too many objects or have a memory leak.
GC algorithm and phase durations: For concurrent collectors like G1GC or ZGC, track concurrent marking time, remark pauses, and evacuation pauses separately.
Modern APM tools surface these metrics in dashboards and alert when thresholds are breached. For example, Java script application monitoring tools track runtime metrics across Node.js and JVM environments, correlating GC behavior with request performance.
Choosing the Right Java Garbage Collector
Java offers multiple garbage collectors, each optimized for different workload characteristics. Choosing the right one depends on your latency requirements, throughput needs, and heap size.
Serial GC
Use case: Single-threaded applications, batch jobs, or memory-constrained environments.
Behavior: Performs all GC work on a single thread. Simple and low overhead but pauses are longer than parallel collectors.
Enable: -XX:+UseSerialGC
When to use: Small heaps (under 100 MB), single-core containers, or scenarios where simplicity matters more than latency.
Parallel GC
Use case: Batch processing, throughput-optimized workloads where occasional long pauses are acceptable.
Behavior: Multi-threaded young and old generation collection. Maximizes throughput but accepts longer pause times.
Enable: -XX:+UseParallelGC
When to use: Applications where total execution time matters more than individual request latency. Common in analytics pipelines and ETL jobs.
G1GC (Garbage First)
Use case: General-purpose collector for server applications with large heaps (4 GB+). Balances throughput and latency.
Behavior: Divides heap into regions and collects regions with the most garbage first. Performs mostly concurrent marking with short stop-the-world pauses. Targets a configurable pause time goal.
Enable: -XX:+UseG1GC (default in Java 9+)
When to use: Web services, microservices, most production applications. G1GC is the best starting point for most teams.
ZGC (Z Garbage Collector)
Use case: Ultra-low latency applications requiring predictable sub-10ms pauses regardless of heap size.
Behavior: Concurrent collector designed for minimal pause times. Performs most GC work concurrently while application threads run. Pause times typically under 1ms even on multi-terabyte heaps.
Enable: -XX:+UseZGC (production-ready in Java 15+)
When to use: Latency-sensitive services where p99 pause times must stay under 10ms. Trading financial systems, real-time APIs, gaming backends.
Shenandoah
Use case: Low latency alternative to ZGC with similar pause time goals.
Behavior: Concurrent collector with concurrent compaction. Pause times independent of heap size, typically under 10ms.
Enable: -XX:+UseShenandoahGC
When to use: Similar to ZGC. Shenandoah may perform better on smaller heaps (under 32 GB) but the difference is workload-specific. Test both if latency is critical.
Tuning G1GC for Production Workloads
G1GC is the most widely used collector in production. Default settings work for many applications, but tuning improves performance for high-traffic services.
G1GC Tuning Parameters
MaxGCPauseMillis: Target pause time in milliseconds. G1GC tries to meet this goal but does not guarantee it.
-XX:MaxGCPauseMillis=100
Setting this too low forces more frequent GCs. Setting it too high allows longer pauses. Start with 100ms and adjust based on observed pause times.
G1HeapRegionSize: Size of each G1 region. Default is calculated based on heap size. Larger regions reduce metadata overhead but may increase pause times.
-XX:G1HeapRegionSize=16m
Use 16 MB for heaps between 8–32 GB. Increase to 32 MB for heaps over 64 GB.
InitiatingHeapOccupancyPercent: Percentage of heap occupancy that triggers concurrent marking. Lower values start marking earlier, reducing the risk of full GC.
-XX:InitiatingHeapOccupancyPercent=35
Default is 45%. Lower to 35% if you see frequent full GCs or memory pressure.
G1ReservePercent: Percentage of heap reserved to reduce promotion failure risk.
-XX:G1ReservePercent=15
Increase if you see “to-space exhausted” errors in GC logs.
ParallelGCThreads: Number of parallel threads for stop-the-world GC phases.
-XX:ParallelGCThreads=8
Set to number of CPU cores available to the JVM. More threads reduce pause times but increase CPU usage during GC.
ConcGCThreads: Number of concurrent GC threads for marking.
-XX:ConcGCThreads=2
Typically set to 1/4 of ParallelGCThreads. More threads speed up concurrent marking but steal CPU from application threads.
Fixed heap size: Set -Xms and -Xmx to the same value to prevent heap resizing overhead.
-Xms4g -Xmx4g
Example G1GC Tuning for a Web Service
java \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=16m \
-XX:InitiatingHeapOccupancyPercent=35 \
-XX:G1ReservePercent=15 \
-XX:ParallelGCThreads=8 \
-XX:ConcGCThreads=2 \
-Xms4g -Xmx4g \
-jar app.jar
This configuration targets 100ms pause times, starts concurrent marking earlier to avoid full GCs, and allocates 8 parallel threads for fast stop-the-world phases.
Enabling GC Logging for Analysis
GC logs provide detailed visibility into collector behavior. Enable logging in production to diagnose performance issues and validate tuning changes.
Enable GC Logging (Java 11+)
java \
-Xlog:gc*=info:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=50m \
-Xlog:gc+heap=debug:file=/var/log/gc-heap.log:time,uptime \
-jar app.jar
This writes GC events to rotating log files, keeping the last 5 files at 50 MB each. The log includes pause times, heap occupancy before and after collection, and GC phase durations.
Analyzing GC Logs
Use these tools to parse and visualize GC logs:
GCViewer: Open-source desktop tool. Graphs pause times, throughput, and heap usage over time.
GCEasy: Free online log analyzer. Upload GC logs to get pause time distributions, memory leak detection, and tuning recommendations.
Eclipse MAT (Memory Analyzer Tool): For heap dump analysis when GC logs indicate a memory leak.
GC log analysis answers:
- Are pause times within acceptable thresholds?
- Is heap usage growing steadily, indicating a leak?
- Are full GCs frequent, suggesting the heap is undersized or the collector is misconfigured?
Monitoring GC Metrics with APM Tools
Manual log analysis works for troubleshooting specific issues, but continuous GC monitoring in production requires APM integration. APM tools collect JVM metrics via agents or OpenTelemetry instrumentation and correlate GC behavior with application performance.
Exposing JVM Metrics Programmatically
The JVM exposes GC metrics through ManagementFactory APIs. APM agents read these and export them to monitoring platforms.
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
public class GCMonitor {
public static void printGCStats() {
List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
System.out.println("GC Name: " + gc.getName());
System.out.println(" Collection count: " + gc.getCollectionCount());
System.out.println(" Collection time: " + gc.getCollectionTime() + "ms");
}
}
}
This code snippet retrieves GC statistics for all collectors running in the JVM. APM agents use similar APIs to export metrics to Prometheus, Datadog, or OpenTelemetry collectors.
Monitoring GC in CubeAPM
CubeAPM monitors JVM runtime metrics including garbage collection pauses, heap usage, and thread states through OpenTelemetry instrumentation. CubeAPM’s self-hosted architecture keeps GC telemetry inside your infrastructure, eliminating public cloud egress costs and meeting data residency requirements.
GC metrics tracked by CubeAPM:
- GC pause count and duration by collector type (young, old, full)
- Heap memory usage (used, committed, max) across young and old generations
- GC throughput percentage (time spent in application code vs. GC)
- Allocation and promotion rates derived from heap behavior
Alerting on GC thresholds: Set alerts when p99 GC pause times exceed 100ms, old generation occupancy stays above 80% for more than 5 minutes, or full GC frequency crosses a defined threshold. CubeAPM correlates GC alerts with distributed traces, so when a GC pause degrades API latency, you see the exact request path affected.
Deployment model: CubeAPM runs inside your VPC or on-prem, processing traces, logs, and metrics locally. For teams monitoring JVM applications across Kubernetes clusters, CubeAPM integrates with OpenTelemetry Java agents to ingest GC metrics without sending data to external SaaS platforms. Pricing is $0.2/GB of ingested telemetry with unlimited retention and no per-seat fees.
Unlike SaaS APM tools that charge per host or user, CubeAPM’s flat ingestion pricing makes it predictable to monitor dozens of JVM services without bill surprises.
Tuning ZGC for Ultra-Low Latency
ZGC is designed for applications where pause times must stay under 10ms regardless of heap size. It performs most GC work concurrently, keeping stop-the-world pauses minimal.
ZGC Configuration Example
java \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:ConcGCThreads=4 \
-XX:+ZUncommit \
-XX:ZUncommitDelay=300 \
-Xms2g -Xmx8g \
-jar app.jar
ZGenerational: Enables generational ZGC (Java 21+), which improves throughput by collecting young objects more frequently than old objects.
ConcGCThreads: Number of threads for concurrent GC work. More threads reduce latency but increase CPU overhead. Set to 1/4 or 1/8 of available cores.
ZUncommit: Allows ZGC to return unused heap memory to the OS. Useful in containerized environments where memory limits are strict.
ZUncommitDelay: Time in seconds before unused memory is uncommitted. Prevents thrashing if heap usage fluctuates frequently.
ZGC does not accept MaxGCPauseMillis because it always targets sub-10ms pauses. Instead, tune heap size and concurrent thread count to balance latency and throughput.
When to Choose ZGC Over G1GC
Use ZGC if:
- Your p99 latency SLA is under 50ms and GC pauses are a bottleneck
- Heap size is large (8 GB+) and G1GC pause times exceed acceptable thresholds
- Your workload benefits from predictable latency more than maximum throughput
Stick with G1GC if:
- Application latency is less sensitive (p99 under 200ms is acceptable)
- Heap size is under 4 GB
- Throughput matters more than pause time consistency
ZGC trades slightly lower throughput for dramatically lower pause times. Benchmark both collectors with your actual workload before choosing.
Common GC Issues and How to Diagnose Them
Frequent Full GCs
Symptom: Full GCs occur multiple times per minute. Application throughput drops significantly.
Cause: Heap is undersized, objects are promoted to old generation too quickly, or a memory leak prevents objects from being collected.
Diagnosis: Check old generation occupancy after full GC. If it remains above 80%, increase heap size or investigate memory leaks with heap dump analysis.
Fix: Increase -Xmx, lower -XX:InitiatingHeapOccupancyPercent to start concurrent marking earlier, or tune application code to reduce long-lived object retention.
Long GC Pause Times
Symptom: Individual GC pauses exceed 200ms, causing latency spikes.
Cause: Large heap, single-threaded GC, or stop-the-world collector like Serial or Parallel GC.
Diagnosis: Review GC logs for pause durations. If using G1GC, check if pause time target is met. If not, consider ZGC.
Fix: Switch to G1GC or ZGC. Increase ParallelGCThreads to speed up stop-the-world phases. Lower MaxGCPauseMillis for G1GC.
High Allocation Rate
Symptom: Young generation collections happen every few seconds. Application creates objects rapidly.
Cause: Code allocates many short-lived objects. Not necessarily a problem unless it triggers excessive GC overhead.
Diagnosis: Profile application with tools like JProfiler or YourKit to identify allocation hotspots.
Fix: Use object pooling for frequently allocated objects. Reuse buffers instead of allocating new ones. Optimize data structures to reduce allocation churn.
Memory Leak
Symptom: Heap usage grows steadily over time. Old generation fills up, triggering frequent full GCs. Eventually, the application runs out of memory.
Cause: Application retains references to objects that should be garbage collected. Common causes include static collections, listener leaks, or unclosed resources.
Diagnosis: Take a heap dump and analyze with Eclipse MAT. Look for large object graphs and dominator trees to identify leak sources.
Fix: Remove references to unused objects. Close resources properly. Use weak references where appropriate.
Premature Promotion
Symptom: Objects are promoted to old generation too quickly, filling it up and causing frequent old generation collections.
Cause: Young generation is too small, or objects survive too many young generation cycles due to long-lived references.
Diagnosis: Check promotion rate in GC logs. High promotion rate indicates objects are not collected in young generation.
Fix: Increase young generation size with -XX:NewRatio or -XX:MaxNewSize. Reduce object lifetimes in application code.
Best Practices for GC Monitoring in Production
Set up continuous GC monitoring: Integrate JVM metrics into your APM platform. Track pause times, heap usage, and GC frequency in real time. Alert when thresholds are breached.
Baseline GC behavior: Measure GC metrics under normal load to establish a performance baseline. Compare future metrics against this baseline to detect regressions.
Tune for your workload: No universal GC configuration works for all applications. Profile your workload, measure pause times and throughput, and iterate on tuning parameters.
Test tuning changes under load: Always validate GC tuning changes in staging or during load tests before deploying to production. A configuration that improves pause times might reduce throughput or cause other issues.
Monitor allocation rate and promotion rate: High allocation or promotion rates signal potential memory inefficiencies. Address these in application code rather than relying solely on GC tuning.
Correlate GC metrics with request latency: Use distributed tracing to link GC pauses with latency spikes in specific requests. This helps prioritize tuning efforts on the most impactful services.
Keep GC logs enabled in production: GC logs have minimal overhead and provide critical data for post-incident analysis. Use log rotation to prevent disk space issues.
Use heap dumps for memory leak investigation: When GC metrics suggest a leak, capture a heap dump and analyze it offline. Do not rely solely on GC logs.
Tools for Java GC Monitoring
CubeAPM: Self-hosted observability platform with native OpenTelemetry support. Monitors JVM metrics including GC pauses, heap usage, and thread states. Correlates GC behavior with distributed traces and logs. Pricing: $0.15/GB ingested data, unlimited retention, no per-seat fees. Best for teams requiring on-prem deployment and data residency compliance.
Datadog: SaaS APM platform with JVM monitoring. Tracks GC metrics, heap usage, and thread pools. Pricing starts at $15/host/month for infrastructure monitoring, with additional costs for APM and logs.
New Relic: Managed observability platform. Monitors JVM performance and GC metrics. Pricing based on data ingestion and user seats, starting at $0.40/GB beyond the free tier.
Prometheus + Grafana: Open-source monitoring stack. Use JMX Exporter or Micrometer to expose JVM metrics to Prometheus. Visualize in Grafana. Free but requires self-hosting and manual setup.
Elastic APM: Part of the Elastic Stack. Monitors JVM metrics and correlates with logs and traces. Free for self-hosted deployments, with managed cloud options starting at $99/month.
AppDynamics: Enterprise APM platform with deep JVM instrumentation. Monitors GC, memory leaks, and thread contention. Pricing starts around $50/vCPU core/month.
For teams already using Cassandra monitoring tools, integrating JVM metrics into the same observability platform simplifies correlation between database performance and application GC behavior.
Conclusion
Java garbage collection monitoring is essential for maintaining predictable application performance in production. GC pauses directly impact user-facing latency, and without monitoring, teams discover problems only after users report slow requests or errors.
Effective GC monitoring tracks pause times, heap usage, allocation rates, and GC throughput, correlating these metrics with request latency and distributed traces. Choosing the right garbage collector depends on your latency requirements, G1GC for balanced workloads, ZGC or Shenandoah for ultra-low latency, and Parallel GC for throughput-heavy batch processing.
Tuning GC improves performance, but monitoring ensures tuning changes work as expected and that GC behavior remains stable as traffic scales. Integrate JVM metrics into your observability stack, set alerts on key thresholds, and use GC logs to diagnose issues when they occur.
Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.
Frequently Asked Questions
What is the difference between minor GC and full GC?
Minor GC collects only the young generation, typically completing in under 10ms. Full GC collects the entire heap including young and old generations, taking longer and causing more noticeable application pauses.
How do I reduce GC pause times in Java?
Switch to a low-latency collector like G1GC or ZGC. Tune heap size, increase parallel GC threads, and lower the GC pause time target. Profile allocation hotspots and reduce object churn in application code.
What causes frequent full GCs?
Frequent full GCs indicate the heap is undersized, objects are promoted to old generation too quickly, or a memory leak prevents garbage collection. Increase heap size or investigate memory leaks with heap dump analysis.
Which garbage collector should I use for a web service?
Start with G1GC, the default in Java 9+. If p99 pause times exceed acceptable thresholds, test ZGC or Shenandoah for sub-10ms pauses.
How do I monitor GC metrics in production?
Integrate JVM metrics into your APM platform using OpenTelemetry or vendor-specific agents. Track pause times, heap usage, and GC frequency. Set alerts when thresholds are breached.
What is a good GC pause time target?
For web services, target p99 pause times under 100ms. For latency-sensitive applications like trading platforms, target under 10ms using ZGC or Shenandoah.
How do I detect a memory leak in Java?
Monitor old generation occupancy after full GCs. If it remains above 80% and grows over time, suspect a memory leak. Capture a heap dump and analyze with Eclipse MAT to identify retained objects.





