GC Logging Analysis: Decoding JVM Garbage Collection Logs
Master JVM garbage collection log analysis with -Xlog:gc, GCEasy, and GCViewer. Practical guide to diagnosing memory issues and tuning performance.
Master JVM garbage collection log analysis with -Xlog:gc, GCEasy, and GCViewer. Practical guide to diagnosing memory issues and tuning performance.
GC Logging Analysis: Decoding JVM Garbage Collection Logs
Garbage collection logs are one of the most valuable sources of information when troubleshooting JVM memory issues. When your application starts exhibiting slowdowns, frequent GC pauses, or memory-related errors, GC logs tell the story that profilers and monitoring tools often cannot. This guide teaches you how to enable comprehensive GC logging, interpret the output, and leverage powerful visualization tools to diagnose performance problems.
Modern JVMs provide extremely detailed GC logging through the -Xlog:gc flag, which replaced the legacy -XX:+PrintGCTimestamps and -XX:+PrintGCDetails flags in Java 9+. The new logging framework is more flexible, allows fine-grained control over what gets logged, and outputs data in a format that tools can parse reliably.
Introduction
When your Java application starts exhibiting slowdowns, frequent GC pauses, or memory-related errors, garbage collection logs tell the story that profilers and monitoring tools often cannot. GC logs record every garbage collection event with timing, memory before and after, pause duration, and the cause of each collection. This information is essential for diagnosing memory issues, tuning GC configuration, and understanding how your application behaves under different allocation patterns.
Modern JVMs provide detailed GC logging through the -Xlog:gc unified logging framework introduced in Java 9, which replaced the legacy -XX:+PrintGCTimestamps and -XX:+PrintGCDetails flags. The new system is more flexible and outputs data in a machine-parseable format that tools like GCEasy and GCViewer can analyze to spot patterns like memory leaks, allocation rate spikes, and metaspace exhaustion.
This guide teaches you how to enable comprehensive GC logging, interpret the output from different GC algorithms, and leverage visualization tools to diagnose performance problems. You will learn what each log entry means, how to identify normal versus problematic GC patterns, and how to correlate GC events with application metrics and deployments to establish baseline behavior and detect anomalies.
When to Use GC Logging / When Not to Use GC Logging
GC logging shines in several scenarios. During performance testing before production release, GC logs reveal whether your heap sizing and collector selection handle your application’s allocation patterns effectively. When investigating production incidents involving memory pressure or latency spikes, GC logs from that timeframe provide crucial forensic data. Capacity planning also benefits enormously from GC log analysis, as historical data shows how memory usage scales with load.
However, GC logging is not always the right tool. In development environments where you are making rapid code changes, the overhead of detailed GC logging can mask actual code-level performance issues. For short-lived applications that complete quickly, the overhead may not justify the insights gained. If latency-critical trading systems require every microsecond, even the minimal overhead of GC logging might be unacceptable during live trading hours—capture logs during off-hours instead.
GC logging also produces substantial data volume. A moderately busy application can generate hundreds of megabytes of log data per day. Plan your log rotation and storage accordingly. For 24/7 production systems, always enable GC logging at a reasonable level; the operational intelligence gained far outweighs the storage cost.
Architecture of JVM Garbage Collection
Understanding how the JVM organizes memory helps you interpret GC logs effectively.
graph TD
subgraph "JVM Heap Memory"
subgraph "Young Generation"
ED[Eden<br/>New objects allocated here]
S0[Survivor Space 0<br/>Survivors from Eden]
S1[Survivor Space 1<br/>Survivors from S0]
end
subgraph "Old Generation"
OG[Old Generation<br/>Long-lived objects promoted]
end
subgraph "Metaspace"
MS[Metaspace<br/>Class metadata, no GC]
end
end
ED -->|"Minor GC<br/>Copying survivors"| S0
S0 -->|"Aging"| OG
S0 <-->|"Copying during GC"| S1
ED -->|"Major/Full GC<br/>Mark-Compact"| OG
The diagram shows the generational memory architecture. Objects allocate in Eden, survive minor GCs in survivor spaces, and eventually promote to the old generation. Understanding this flow helps you interpret why certain GC events occur and what they mean for your application.
Enabling GC Logging
Modern Java versions use the unified logging system introduced in Java 9. Enable GC logging with the -Xlog flag:
# Basic GC logging to file
java -Xlog:gc*:file=gc.log:time,uptime -jar your-application.jar
# Detailed logging with tags for different GC phases
java -Xlog:gc*=info:file=gc-detail.log:time,uptime,tags -jar your-application.jar
# Minimal overhead production logging
java -Xlog:gc=info:file=gc-production.log:time,uptime -jar your-application.jar
# Log rotation configuration
java -Xlog:gc*:file=gc-%t.log:time,filecount=10,filesize=100M -jar your-application.jar
The format breaks down as follows. gc* selects all GC-related log messages. file=gc.log specifies the output file. time,uptime,tags control what information appears in each line. The filecount and filesize options enable automatic rotation, critical for production systems.
For Kubernetes deployments, direct logging to stdout so the container orchestrator can handle rotation:
java -Xlog:gc*=info:stdout -jar your-application.jar
Understanding GC Log Output
A typical minor GC event looks like this:
[2026-05-26T10:15:32.145+0000][29845.432][gc ] GC(12) Pause Young (Normal)
GC(12) Heap before: used=245M, committed=512M, allocated=512M
GC(12) young: 245M -> 0B; [Metaspace: 48M -> 48M]
GC(12) Heap after: used=89M, committed=512M, allocated=512M
GC(12) old: 67M -> 89M; [Metaspace: 48M -> 48M]
This shows a minor GC that reclaimed 156 MB from the young generation. The old generation grew slightly from 67 MB to 89 MB because some long-lived objects survived. The pause lasted long enough to be logged but was not severe enough to cause concern.
A Full GC event requires more attention:
[2026-05-26T10:15:45.678+0000][29858.965][gc ] GC(13) Pause Full (Allocation Failure)
GC(13) Heap before: used=471M, committed=512M, allocated=512M
GC(13) metaspace: 52M -> 52M
GC(13) Heap after: used=156M, committed=512M, allocated=512M
GC(13) old: 423M -> 108M; [Metaspace: 52M -> 52M]
Full GCs occur when the old generation cannot accommodate an allocation request or when the permanent generation fills. The pause time on Full GCs is typically an order of magnitude greater than minor GCs, making them the primary culprit in latency-sensitive applications.
GC Log Analysis Tools
GCEasy
GCEasy is a web-based tool that accepts uploaded GC logs and produces detailed analysis. It identifies the GC algorithm in use, calculates pause times and throughput, detects memory leak trends, and provides actionable recommendations.
For logs stored in a file:
# Upload to GCEasy (requires API key for automation)
curl -F "file=@gc.log" https://api.gceasy.io/analyzeGC
# Or use the web interface
# https://gceasy.io
GCEasy excels at identifying problems that require experience to spot. It recognizes patterns like “metaspace exhaustion,” “allocation rate spikes,” and “degenerate GCs” that indicate collector misconfiguration.
What GCEasy shows you depends on the logging level you enabled. With basic -Xlog:gc=info, GCEasy extracts pause times, GC cause, and memory usage. Crank it up to -Xlog:gc*=trace and it reconstructs the full GC timeline: concurrent phases, reference processing duration, card table flush times. The more detail you capture, the richer the analysis.
The tool organizes findings into tabs. The GC Stats tab aggregates key metrics across all GC events: total GC time, longest pause, average pause, throughput percentage. The Heap Dump tab plots heap usage over time so you can spot the sawtooth pattern of a healthy minor GC versus the staircase climb of a memory leak. The Cause Graph tab maps GC cause to frequency, helping you distinguish normal allocation-driven GC from pathological triggers like “Last_ditch_collection” or “Allocation Failure” firing dozens of times per minute.
GCEasy also generates a recommendations section that reads like a GC tuning cheat sheet. It suggests heap size adjustments, flags excessive Full GC frequency, and recommends collector switches based on your pause time targets. Treat these as starting points, not gospel—validate recommendations against your actual workload before applying them.
Here is a practical workflow: pull the GCEasy URL from the API response and embed it in your incident management tool. When an alert fires, the analyst gets the full GC report without manually uploading logs or opening a browser. This matters at 3 AM when speed matters more than learning the tool from scratch.
GCViewer
GCViewer is an open-source desktop tool for visualizing GC logs. It renders charts showing heap usage over time, pause time distribution, and throughput calculations.
# Download from GitHub releases
wget https://github.com/chewiebug/GCViewer/releases/download/v1.34.1/standalone-javafx-1.34.1.jar
# Run with Java 11+
java -jar gcviewer-1.34.1.jar gc.log
Key metrics GCViewer provides include total pause time, average pause time, maximum pause time, and throughput percentage. The visual representation makes it easy to spot problem periods that correlate with application behavior or deployment events.
Reading the GCViewer charts takes some familiarity with what each view shows. The heap usage chart plots used memory against time on the Y-axis, with one line per heap generation. A healthy minor GC pattern shows a repeating spike-and-dip waveform on the young generation line. The old generation line should stay relatively flat if your application has stable live data. If it climbs steadily after each Full GC, you have a promotion or leak problem. The pause time chart renders each GC pause as a vertical bar. Height matters more than width here, a tall bar means a long pause. Clustered tall bars during specific time windows point directly at GC causing latency spikes in your application.
GCViewer also provides a throughput gauge. The math is simple: (total_time - total_gc_pause_time) / total_time. Below 90% and you have a GC problem worth investigating. Below 80% and you are losing more than a fifth of your CPU to memory management, which is a significant tax on your application’s efficiency.
GCViewer has one advantage over GCEasy: comparing two log files side by side. Run your application with the old settings for an hour, then with the new settings, and load both logs into GCViewer. The comparison view overlays heap usage and pause time, making it immediately obvious whether the tuning helped. This is the fastest way to validate a hypothesis before committing a change to production.
The tool exports data as CSV, which comes in handy if you want to feed GC metrics into your own dashboards or build custom analysis that GCViewer does not support natively. Pair it with Grafana and you have a homegrown GC monitoring solution that never sends sensitive logs to external services.
Other Tools
Beyond GCEasy and GCViewer, a few other tools fill specific niches.
GCPlot is for teams that want programmatic access to GC metrics. It runs as a self-hosted server or cloud service, accepting log uploads via API and exposing metrics through a dashboard. If you already use Grafana or Prometheus, you can wire GCPlot data through its REST API. It supports threshold alerting on pause times and throughput drop.
IBM GC Extension for Health Center targets WebSphere and IBM JVM environments. It pulls GC data alongside CPU, memory, and I/O metrics into one view, so you can see GC behavior in the context of system resource consumption. On AIX or Windows servers running traditional IBM Java stacks, this gives you more relevant data than generic tools.
Amazon Corretto bundles its own diagnostic commands and hooks into AWS CloudWatch. The jcmd utility that comes with Corretto exposes GC cause and phase information, which pairs naturally with CloudWatch dashboards. If you run Java on EC2 or containers in AWS, the built-in diagnostics cover most steady-state monitoring needs without third-party tools.
Caution on External Tools: Uploading production GC logs to external services exposes your application’s memory patterns, class names, and potentially sensitive data to networks you do not control. Before sending logs to a tool like GCEasy, check their data retention policy and confirm your compliance requirements allow it.
Production Failure Scenarios
Scenario 1: Continuous Full GC Due to Memory Leak
A production service suddenly starts experiencing Full GC every few minutes. The old generation fills up immediately after each Full GC, causing another. This classic pattern indicates objects leaking into the old generation—typically caches without eviction, static collections, or object pools that grow unboundedly.
Look for Full GC events firing in rapid succession, often with the same GC cause repeated. The old generation memory shows no meaningful reduction after each collection. Compare consecutive Full GC entries:
[2026-05-26T14:23:01.123+0000][48234.556][gc] GC(47) Pause Full (Allocation Failure)
GC(47) Heap before: used=489M, committed=512M, allocated=512M
GC(47) Heap after: used=156M, committed=512M, allocated=512M
GC(47) old: 441M -> 108M
[2026-05-26T14:23:45.891+0000][48278.324][gc] GC(48) Pause Full (Allocation Failure)
GC(48) Heap before: used=471M, committed=512M, allocated=512M
GC(48) Heap after: used=152M, committed=512M, allocated=512M
GC(48) old: 423M -> 104M
Notice the old generation sits at 423-441 MB before each Full GC despite reclaiming 330+ MB each time. This means roughly 400 MB of live objects return to the old generation almost immediately. After several cycles with no relief, a memory leak is confirmed.
To diagnose, capture heap dumps at two points during leak progression using jcmd <pid> GC.heap_dump heap_before.hprof and again after a few more Full GCs. Compare the dumps with jhat or VisualVM to find object instances that grew significantly. Run jmap -histo:live <pid> after a Full GC to see the histogram of live objects, looking for unexpected class names or large primitive arrays. Enable object allocation tracking with -XX:TLABSize=512k -XX:+HeapDumpOnOutOfMemoryError to capture where leaked objects originate.
Common sources of leaks: unbounded caches using ConcurrentHashMap without size limits. Static List, Map, or Set collections populated at runtime and never cleared. Connection pools with no max size. ThreadLocal instances not removed in web applications. Object pools for reuse that accumulate rather than release.
Scenario 2: GC Thrashing Under High Allocation Rate
An application with bursty traffic experiences severe performance degradation during traffic spikes. GC logs show extremely frequent minor GCs with high allocation rates, sometimes followed by Full GCs when the young generation cannot keep up.
The defining characteristic is a high ratio of minor GCs to application time. You see “Pause Young (Normal)” events firing every few seconds or more. The survivor spaces fill up fast, and objects promote prematurely to the old generation.
[2026-05-26T15:04:12.234+0000][51234.123][gc] GC(89) Pause Young (Normal)
GC(89) Heap before: used=478M, committed=512M, allocated=512M
GC(89) young: 478M -> 0B
GC(89) Heap after: used=67M, committed=512M, allocated=512M
GC(89) old: 411M -> 467M; [Metaspace: 52M -> 52M]
The old generation promotion line tells the story: 411 MB promoted to 467 MB during a single minor GC. That is 56 MB of objects that should have died in the young generation. Over time, the old generation fills, triggering Full GCs under allocation pressure.
To confirm, calculate the allocation rate by multiplying minor GC frequency by promotion volume. If promotion consistently exceeds 20% of the young generation size per minor GC, your young generation is undersized or the survivor spaces are too small to hold aging objects. Use GCViewer to visualize the heap usage chart. You will see a sawtooth pattern on the young generation line with sharp peaks followed by frequent dips.
To fix it, increase the young generation size with -Xmn512m (or higher) to give short-lived objects more room to die before promotion. Adjust the survivor space ratio with -XX:SurvivorRatio=6 to increase survivor capacity relative to Eden. If promotion continues despite larger survivor spaces, consider -XX:MaxTenuringThreshold=15 to age objects more before promotion. For bursty allocation patterns, use a collector with better worst-case behavior like G1 or ZGC rather than the default throughput collector.
One thing to watch: if increasing the young generation causes Full GC frequency to rise instead of fall, the problem is not allocation rate but object lifetime. Your application is holding onto objects that should have died, filling the heap regardless of young generation sizing. In that case, find what your application is holding onto instead of tuning GC.
Scenario 3: Metaspace Exhaustion
Modern applications using reflection, dynamic proxies, or heavy classloading can exhaust Metaspace. Before Java 8, this would have been PermGen space. Metaspace grows dynamically but garbage collects classloaders when they become unreachable.
Logs show Metaspace increasing steadily, then hitting the configured limit. If MaxMetaspaceSize is set too low, you will see “OutOfMemoryError: Metaspace” errors.
How to identify it: Watch the Metaspace line in GC output. If it grows continuously with no corresponding decrease after GC, you have metaspace pressure. Heap memory typically reclaims space after collection; metaspace does not behave the same way. When Metaspace: used=X -> Y shows Y getting close to the configured maximum with no relief after classloader GC phases, you are hitting the ceiling.
Common culprits: ORM frameworks like Hibernate generate proxy classes at runtime. Serialization libraries like Kryo or Jackson create class variants for generic types. JSP compilers load a new classloader per page compilation. Dynamic proxies via java.lang.reflect.Proxy and bytecode manipulation libraries like ASM, CGLIB, or javassist all add classloader pressure. After a hot redeployment, old classloaders sometimes linger because static references keep them alive.
Diagnosis steps: Run jcmd <pid> VM.native_memory summary to see the native memory breakdown. Use jmap -clstats <pid> to list loaded classes and their classloaders. Enable -XX:+TraceClassLoading temporarily to see which frameworks load the most classes. Compare metaspace usage before and after redeployments; if it grows with each deploy, you have a classloader leak.
Remediation: Bump -XX:MaxMetaspaceSize if the limit is artificially low. For classloader leaks, find static references to classloaders in your shutdown hooks and null them out. G1 handles metaspace GC more gracefully than older collectors, so -XX:+UseG1GC often helps. If dynamic class generation is central to your application, GraalVM native image is worth evaluating since class metadata becomes static at build time.
Trade-off Table
| Aspect | Minor GC | Full GC | G1 GC | ZGC |
|---|---|---|---|---|
| Pause Time | 1-50ms typically | 100ms-2s | <200ms target | <1ms target |
| Throughput Impact | Low | High | Medium | Minimal |
| Memory Efficiency | Good for short-lived objects | Poor for large heaps | Adaptive | Excellent |
| Configuration Complexity | Medium | Low | High | Medium |
| Best For | Batch workloads | Small heaps | Responsive apps | Ultra-low latency |
Implementation Snippets
Analyzing GC Logs Programmatically
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.regex.*;
public class GcLogAnalyzer {
// Pattern for pause time extraction
private static final Pattern PAUSE_PATTERN =
Pattern.compile("Pause (Young|Old|Full).*?(\\d+\\.\\d+)ms");
public static void main(String[] args) throws Exception {
double totalPauseTime = 0;
int gcCount = 0;
try (BufferedReader reader = new BufferedReader(
new FileReader("gc.log"))) {
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = PAUSE_PATTERN.matcher(line);
if (matcher.find()) {
double pauseTime = Double.parseDouble(matcher.group(2));
totalPauseTime += pauseTime;
gcCount++;
System.out.printf("GC #%d: %.2fms%n", gcCount, pauseTime);
}
}
}
System.out.printf("%nTotal GC pauses: %d%n", gcCount);
System.out.printf("Average pause: %.2fms%n", totalPauseTime / gcCount);
System.out.printf("Total pause time: %.2fms%n", totalPauseTime);
}
}
Automated Log Upload
At scale, manually uploading logs to GCEasy does not scale. Automating the pipeline lets you analyze GC logs continuously without someone doing it by hand.
When to automate: If you produce more than 1 GB of GC logs per day, manual analysis is not sustainable. Scheduled uploads every few hours, or triggered after each log rotation, keep your analysis current. Teams working incidents can also fire an upload immediately from CI/CD hooks or monitoring alerts.
Script mechanics: The bash script checks if the log file exists before attempting upload, which prevents failures from missing files. The API key authenticates your request to GCEasy’s server. The response JSON contains a gceasyUrl field with a link to the analyzed report, or an error field if something went wrong. Using jq -r to extract the field handles both cases cleanly.
#!/bin/bash
# Upload recent GC logs to GCEasy for analysis
LOG_FILE="gc-production.log"
if [ -f "$LOG_FILE" ]; then
RESPONSE=$(curl -s -F "file=@$LOG_FILE" \
-F "apiKey=YOUR_API_KEY" \
https://api.gceasy.io/analyzeGC)
echo "$RESPONSE" | jq -r '.gceasyUrl // .error'
else
echo "GC log file not found: $LOG_FILE"
fi
Production considerations: Store your API key in a secrets manager, never hardcode it in scripts. Add retry logic with exponential backoff for network failures. Store the gceasyUrl in your incident management system so engineers can access the report without needing GCEasy access themselves. In security-sensitive environments, consider self-hosting GCEasy or using the on-premises version.
CI/CD integration: Wire the upload into your deployment pipeline to automatically analyze GC logs after each production deploy. Correlate the GCEasy report URL with the deployment timestamp in your monitoring dashboard so GC health shows up alongside your standard deployment metrics.
Observability Checklist
Before declaring a GC issue resolved, verify these points. Throughput should meet your SLAs—typically 95% or higher for production services. Pause times should remain below your latency budget; for most applications, sub-200ms pauses are acceptable. No continuous Full GC cycles should occur. Memory usage should stabilize after warmup, not grow unboundedly. Object promotion rates should be reasonable relative to your application lifetime.
Monitor these metrics over at least one full load cycle to ensure the patterns observed are consistent and not anomalies.
Security and Compliance Notes
GC logs contain sensitive information about your application’s memory usage patterns. Object class names can reveal implementation details, and string content in objects may expose user data or internal system information. Protect GC logs accordingly—encrypt them at rest and restrict access to operations and performance teams.
In regulated industries, GC logs may fall under operational audit requirements. Document why you enabled specific logging levels and retain logs according to your data retention policy.
Never expose GC log analysis tools to untrusted networks. Tools like GCEasy process sensitive memory data; ensure you understand their data handling policies before uploading production logs.
Common Pitfalls / Anti-Patterns
The most frequent mistake is enabling too much logging in production. Verbose GC logging with all tags enabled creates significant overhead and generates enormous log volumes. Start with minimal logging and add tags only when investigating specific issues.
Another common error is analyzing logs from the wrong time window. GC behavior during application startup differs dramatically from steady-state operation. Always compare logs from similar application lifecycle phases.
Misinterpreting Full GC causes leads to ineffective solutions. Not every Full GC indicates a problem. Some Full GCs are normal—metaspace garbage collection, or Java heap dumps triggered programmatically, or System.gc() calls from application code.
Finally, tuning GC without load testing produces misleading results. What works under test load may fail under production traffic patterns. Always validate GC tuning changes with realistic workloads.
Quick Recap Checklist
- Enable GC logging in production with
-Xlog:gc=info:file=gc.log - Use log rotation to manage volume:
filecount=10,filesize=100M - Analyze logs with GCEasy for automated insights and GCViewer for visualization
- Distinguish normal GC patterns from problematic ones
- Correlate GC events with application metrics and deployment events
- Test any tuning changes under realistic load before production deployment
- Protect GC logs—they contain sensitive memory information
Interview Questions
Minor GC collects the young generation—Eden and survivor spaces. It runs frequently because the young generation fills quickly with newly allocated objects. Minor GC uses the copying algorithm, moving surviving objects between survivor spaces or promoting them to the old generation. It is usually fast, typically 1-50ms.
Full GC collects the old generation and sometimes triggers concurrently with the young generation. It uses mark-compact algorithms that are inherently slower because they must identify all live objects across the entire heap, not just the young generation. Full GC pauses can last 100ms to several seconds depending on heap size and object density. Frequent Full GCs indicate memory pressure or object promotion issues.
Look for a pattern where the old generation usage continuously increases over time without corresponding decreases after Full GC events. If after multiple Full GCs the old generation never shrinks back down, objects are leaking into it.
Steps: First, enable verbose GC logging. Second, analyze the old generation usage trend over several days using GCEasy or GCViewer. Third, generate heap dumps at two points in time and compare them to identify growing object graphs. Fourth, use jmap -histo to find unexpectedly large object arrays or class instance counts. Common culprits include static collections, caches without size limits, unbounded object pools, and unclosed resources like database connections stored in instance variables.
For Java 9 and later, use the unified logging flag: -Xlog:gc=info:file=/path/to/gc.log:time,uptime,filecount=5,filesize=50M. This captures GC information at info level, includes timestamps and uptime, and rotates logs to prevent disk exhaustion.
Avoid overly verbose settings like gc*=trace in production because the overhead impacts performance and generates excessive data. For Java 8 and earlier, the equivalent is -XX:+PrintGCTimestamps -XX:+PrintGCDetails -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=50M.
It indicates a minor GC event in the young generation that completed normally. "Pause" refers to the stop-the-world pause that occurs when application threads are temporarily halted for the GC to operate safely. "Young" confirms the collection targeted the young generation. "Normal" means the collection succeeded without issues like promotion failure.
The log shows before and after heap states: Heap before: used=245M to Heap after: used=89M tells you how much memory was reclaimed. The old generation line shows promotion during the minor GC—some objects were old enough to promote rather than die.
Several indicators suggest GC tuning is the solution. Throughput remains below SLA despite optimal code—GC overhead is consuming the budget. Pause times exceed latency SLAs even with healthy application code. The old generation grows continuously but holds mostly long-lived, legitimately cached data that application code cannot reduce.
If heap usage is stable and reasonable after warmup but GC runs too frequently, increasing heap or adjusting generation ratios helps. If allocation rate analysis shows short-lived objects promoting unnecessarily, tuning survivor space ratios helps. If pauses cluster during specific operations, the allocation profile may need adjusting rather than code changes.
G1GC logs show "Pause Young" and "Pause Full" events with region-level detail showing which regions were collected and how much memory was reclaimed per region type. The output includes heap state before and after with young/old generation breakdowns.
ZGC logs show concurrent phases: "Pause Remroot," "Concurrent Move," "Concurrent Mark," etc. Because ZGC is mostly concurrent, its pauses are much shorter. The logs reveal which phases caused the brief stop-the-world moments and the throughput impact of concurrent phases.
The time keyword in -Xlog adds wall-clock timestamps (ISO 8601 format). The uptime keyword adds seconds since JVM start. For correlating GC events with application behavior, uptime is more useful because it is independent of system clock changes or log rotation.
To correlate, match GC pause times from logs against application metrics (request latency spikes, throughput drops) and deployment events (app restarts, config changes). A GC pause that aligns with a latency spike confirms it as the root cause.
Concurrent Mode Failure occurs in G1 or CMS when the concurrent marking phase cannot complete before the old generation fills up. The JVM must interrupt application threads to perform a Full GC. This indicates the heap is too small for the application's live set, or the allocation rate is higher than the concurrent collector can handle.
Solutions include increasing heap size, adjusting the mixed GC ratio with -XX:G1OldGCCountInterval, or switching to a collector better suited to high allocation rates like ZGC.
Allocation Failure is the most common reason for a young generation GC. It occurs when a thread tries to allocate an object in the young generation but finds no space. This triggers a minor GC to free space. This is normal behavior, not a problem—the term "failure" here means the allocation attempt failed and triggered GC, not that something went wrong.
High frequency of Allocation Failure followed by Full GC (Allocation Failure) indicates memory pressure where objects are promoting too quickly or the young generation is too small.
GC overhead = (Total GC pause time / Total application uptime) * 100. GCViewer calculates this automatically. A healthy application typically shows less than 5% GC overhead. Above 10% indicates GC is consuming too much application time.
For example, if your application runs for 1 hour (3,600,000ms) and accumulates 180,000ms of GC pause time, overhead is 5%. This calculation helps set GC tuning goals and measure improvement after tuning changes.
Metaspace stores class metadata and is separate from the Java heap. Increasing metaspace with stable heap indicates classloader memory growth—typically from dynamic class generation, reflection-heavy frameworks, or a classloader leak after redeployment. This is different from heap memory leaks and requires analysis of classloader retention, not object retention.
Check for frameworks that generate classes dynamically: ORM systems, serialization libraries, JSP compilers, scripting engines. If metaspace grows continuously without stabilising, increase -XX:MaxMetaspaceSize or investigate why classloaders are not being garbage collected.
A degenerate GC is an emergency GC that occurs when the JVM cannot allocate memory even after a Full GC. It is typically triggered after "Concurrent Mode Failure" when the CMS or G1 collector cannot keep up with allocation rate. The JVM falls back to a stop-the-world mark-compact operation that is often much slower than a regular Full GC.
Degenerate GCs indicate serious memory pressure. The fix is either increasing heap, adjusting GC parameters to trigger mixed collections earlier, or switching to a collector like ZGC designed to avoid this scenario.
If after every Full GC the old generation quickly returns to near-full capacity, the heap is too small for the application's live set. If the young generation is too small, minor GCs occur very frequently with high allocation rates. If survivor spaces are consistently full after minor GC, objects are promoting before they should.
Look at the "Heap after" values for each GC type and compare them to "Heap before" to see how much headroom exists. A healthy GC pattern shows stable used memory after GCs with room to grow before the next collection.
Shenandoah logs show concurrent GC phases similar to ZGC but with different naming. You will see "Pause Init" (initial marking), "Concurrent Mark," "Pause Final Mark," and "Concurrent Cleanup." The key difference from G1 is that Shenandoah performs evacuations (moves objects) concurrently rather than during stop-the-world pauses.
The critical metric in Shenandoah logs is the evacuation pause times—these should be consistently sub-millisecond. If evacuation pauses grow large, it indicates issues with the collection set or memory layout.
GC logs rotate based on filecount (number of files to keep) and filesize (size threshold for rotation). Tools like GCViewer and GCEasy handle rotated logs differently: GCViewer can process multiple files if provided as arguments or merged, while GCEasy may process only the most recent file.
For continuous production monitoring, use a cron job or log shipper to process rotated files and feed them into a centralized system. Never rely solely on the currently active log file—it may not contain enough historical context for meaningful analysis.
The number in parentheses represents the GC cycle ID, an incrementing counter across the JVM lifetime. Tracking GC cycle IDs helps correlate events across log timestamps. If GC(12) is followed by GC(15), you know some cycles were not logged at the verbosity level you selected. The ID also helps track how many GCs occurred between events you are investigating.
Establish a baseline by measuring current GC overhead percentage, average pause time, and maximum pause time. After tuning changes, compare these metrics. Success criteria: GC overhead reduced below 5% (from higher), average pause time reduced to within your SLA target, and no continuous Full GC cycles. Run the workload long enough to capture representative behavior before declaring success.
The "Old Gen" line in GC log output shows how much memory was promoted from young to old generation during the GC. For minor GCs, it shows old generation before and after values. Rapid old generation growth indicates objects promoting too quickly, which could mean survivor spaces are too small or objects have too long a lifespan for your young generation sizing.
The GC cause field (e.g., "Allocation Failure", "G1 Evacuation Pause", "Heap Dump") explains why the GC was triggered. Common causes include Allocation Failure (young gen full), Heap Dump (triggered by jmap or similar), System.gc() (explicit call), and Metaspace GC (classloader cleanup). Unusual causes or frequent triggers help diagnose whether GC is working as expected or if something is causing excessive GC.
For capacity planning, track old generation growth rate over time. If old gen usage grows consistently after each Full GC, you have a memory leak. Calculate the growth rate in MB/hour to predict when the heap will be insufficient. Also analyze allocation rate (bytes/second being allocated) to determine minimum heap needed for your workload. Historical GC data helps size heaps for future traffic growth.
Further Reading
- JVM Flags Explained — Configure the flags that generate the logs analyzed here
- Java Mission Control — Advanced flight recording analysis for deeper JVM diagnostics
- GCEasy: Free Online GC Log Analyzer — Upload GC logs for automated analysis and recommendations
- Oracle Java Docs: GC Tuning Guide — Official GC tuning documentation
- GCViewer on GitHub — Open-source desktop visualization tool for GC logs
- Advanced Java & JVM Internals Roadmap — Parent roadmap
Conclusion
GC logging remains one of the most valuable diagnostic tools for understanding JVM memory behavior. Enable -Xlog:gc=info in production with appropriate log rotation to capture the data you need for incident investigation. Use GCEasy or GCViewer to analyze logs and identify patterns like memory leaks, allocation rate issues, and metaspace exhaustion.
Correlate GC events with application metrics and deployments to establish baseline behavior and detect anomalies. For memory issues, GC logs tell the story that profilers cannot. The investment in proper log configuration pays off during production incidents when you need to understand exactly what the JVM was doing at the moment of failure.
Category
Related Posts
Heap Walking and Allocation Tracking: TLABs and Heap Analysis
Understand how the JVM allocates memory with TLABs, how to track allocations with low overhead, and how heap walking tools analyze object graphs.
Java Atomics and VarHandle: Low-Level Concurrency
Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.
JVM Bytecode Verification: Type Checking and Stack Map Frames
A technical deep dive into the JVM bytecode verifier, covering type checking, stack map frames, the four verification stages, and what happens when verification fails.