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.

published: reading time: 22 min read author: GeekWorkBench

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:

```bash
# 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.

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.

Other Tools

GCPlot is a self-hosted or cloud-based option for teams that want to analyze logs programmatically. IBM GC Extension for Health Center provides deep analysis for WebSphere environments. Amazon Corretto users have access to the Amazon инструменты for GC analysis in AWS environments.

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.

Diagnosis involves comparing heap dumps before and after a Full GC cycle. Look for objects that should not survive minor GC but appear in the old generation. The jmap -histo command helps identify large object arrays or unexpected class instances.

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.

This indicates the heap is too small for the application’s allocation pattern. The young generation especially suffers because objects promoting to old generation include ones that should have died in survivor spaces.

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.

Trade-off Table

AspectMinor GCFull GCG1 GCZGC
Pause Time1-50ms typically100ms-2s<200ms target<1ms target
Throughput ImpactLowHighMediumMinimal
Memory EfficiencyGood for short-lived objectsPoor for large heapsAdaptiveExcellent
Configuration ComplexityMediumLowHighMedium
Best ForBatch workloadsSmall heapsResponsive appsUltra-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

#!/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

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

1. What is the difference between a Minor GC and a Full GC in the context of JVM garbage collection?

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.

2. How would you diagnose a memory leak in a Java application using GC logs?

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.

3. What JVM flags would you use to enable GC logging in a production environment and why?

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.

4. What does the GC log entry "Pause Young (Normal)" mean?

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.

5. What are the signs that a Java application needs GC tuning rather than code changes?

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.

6. What is the difference between G1GC and ZGC log output?

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.

7. How do you interpret GC log timestamps and use them for correlation?

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.

8. What does "Concurrent Mode Failure" mean in GC logs?

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.

9. What is the "Allocation Failure" trigger in GC logs?

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.

10. How do you calculate GC overhead percentage from logs?

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.

11. What does it mean when GC logs show metaspace increasing but heap remaining stable?

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.

12. What is the difference between a "Degenerate GC" and a regular Full GC?

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.

13. How do you use GC logs to identify incorrect heap sizing?

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.

14. What is the meaning of GC log output from Shenandoah?

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.

15. How does log rotation interact with GC log analysis tools?

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.

16. What do GC log entries like "GC(12)" represent and how do you track them?

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.

17. How do you identify GC tuning success using log analysis?

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.

18. What does the "Old Gen" line in GC logs tell you about promotion?

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.

19. What is the significance of "GC cause" in log entries?

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.

20. How do you analyze GC logs for capacity planning?

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

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.

#jvm #heap #tlab

Java Atomics and VarHandle: Low-Level Concurrency

Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.

#java #jvm #concurrency

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.

#java #jvm #bytecode