Why Did Our Java Service Trigger Full GC? Uncovering Log4j2’s Hidden Memory Leak
After a recent deployment, a Java service began experiencing over five Full GC events per minute, traced to Log4j2’s thread‑local buffer misconfiguration and a -XX:PretenureSizeThreshold setting that forced large StringBuilder objects directly into the old generation, leading to memory pressure and frequent Full GCs.
Summary
This article records a production incident where a Java service started generating frequent Full GC events. The root cause was a combination of Log4j2 configuration, JVM parameters, and a newly introduced Servlet dependency that disabled Log4j2’s thread‑local cache, causing large StringBuilder objects to be allocated directly in the old generation.
1. Incident Origin
Shortly after a nightly deployment, operations reported that Full GC alerts appeared, with more than five Full GC occurrences per minute. The affected system uses Log4j2 version 2.31.8.
1.1 JVM Monitoring Dashboard
The Full GC frequency spiked immediately after the release, indicating a direct correlation with the new version. A rollback was considered while a memory dump was collected for further analysis.
2. GC Log Analysis
GC logs reveal: 82 Minor GC events and 105 Full GC events.
High Minor GC frequency shows frequent short‑lived object creation.
Full GC frequency exceeds Minor GC, indicating severe memory allocation imbalance:
AI‑generated table summarises memory usage before and after GC:
Memory Region
Before GC
After GC
Trend
Eden
100% (3.13GB full)
30% (≈0.94GB)
↓70%
Survivor
0% (idle)
0% (idle)
inactive
Old (CMS)
96.2% (1.84GB/1.87GB)
33.3% (0.65GB/1.87GB)
↓63% (still high)
Key observation: after a Young GC the old generation still holds 0.65 GB, a typical sign of memory leak or improper promotion.
2. Investigation
AI analysis suggested that large objects were being created and directly promoted to the old generation, prompting a deeper root‑cause investigation.
2.1 Review Deployment Changes
The release removed the old Dubbo dependency and introduced an internal xxx‑rpc framework. No business code changes were made. Initial suspicion fell on the new RPC framework, but it is widely used without issues, so the code diff did not reveal the problem.
2.2 Memory Dump Analysis
Memory snapshots were examined. Objects were filtered by package name; the count of objects seemed normal. Sorting by class count highlighted char[] arrays and String objects as the most numerous and memory‑heavy, pointing to extensive string handling (e.g., logging, JSON serialization).
Further inspection of incoming references showed many StringBuilder instances with a fixed buffer size of over 2 MB.
The log framework creates a large StringBuilder (capacity defined by MAX_REUSABLE_MESSAGE_SIZE) for each log call, filling the buffer with empty characters to reach the preset size (1 MB). This explains the massive
