Resolving High Memory Utilization after Upgrading to JDK 11 with G1GC: Causes, Analysis, and Tuning Guide
After upgrading to JDK 11 and switching to G1GC, the author observed memory usage exceeding 90% due to G1 Old GC not reclaiming space, and provides a detailed analysis of the issue, including heap inspection, GC mechanisms, and practical tuning solutions such as adjusting heap size and G1GC parameters.
The article documents a real‑world incident where upgrading a production service from JDK 8 (using CMS) to JDK 11 (using G1GC) caused memory utilization to climb above 90% and trigger alarms.
Background : The upgrade was performed via an automated pipeline, setting the JVM heap to -Xms12g -Xmx12g . Initial tests showed reduced GC counts, but after half a month of stable operation the memory usage spiked and G1 Old GC activity increased without reclaiming space.
Investigation : Using jhsdb jmap --heap --pid the author examined the heap layout and discovered that the entire 12 GB heap was being touched, with the old generation not being reclaimed. A temporary fix involved restarting a beta instance and keeping the rest of the fleet restarted, confirming that the issue was tied to G1 Old GC not triggering.
Final Solution : Reducing the heap size to 8 GB (e.g., -Xms8g -Xmx8g ) resolved the problem. The author notes a general recommendation: for an 8 GB container, set the heap to ~4 GB; for a 16 GB container, set the heap to ~8 GB.
GC Fundamentals : The article explains Java memory allocation techniques—pointer bump ("pointer collision") and free‑list allocation—and how they interact with multithreaded allocation via TLAB (Thread‑Local Allocation Buffer). It then describes generational collection, marking‑compact, copying, and mark‑sweep algorithms, and why most objects die young.
G1GC Mechanics : G1 divides the heap into equal‑sized Regions (Eden, Survivor, Old, Humongous). It alternates between a Young‑only phase (minor GC) and a Space‑reclamation phase (mixed GC) that incrementally reclaims old‑generation space. The article details the concurrent start, remark, and cleanup steps, as well as the role of Remembered Sets (RSet) and card tables.
Comparison with CMS : CMS uses a fixed‑address old‑generation allocation and suffers from fragmentation, while G1’s region‑based approach touches the whole heap more aggressively, leading to higher memory footprint but better fragmentation handling. Performance numbers from SPECjbb2015 show JDK 11 + G1 achieving lower pause times and higher throughput compared to JDK 8 + CMS.
Tuning Recommendations :
Adjust heap size to avoid over‑provisioning (e.g., 8 GB heap for a 16 GB container).
Use -XX:+UseG1GC and set region size, e.g., -XX:G1HeapRegionSize=16m .
Configure Initiating Heap Occupancy Percent (IHOP) to control when concurrent marking starts, e.g., -XX:InitiatingHeapOccupancyPercent=40 and optionally disable adaptive IHOP with -XX:-G1UseAdaptiveIHOP .
Limit heap waste with -XX:G1HeapWastePercent=2 to reduce unnecessary mixed GC cycles.
Consider -XX:+AlwaysPreTouch to pre‑allocate physical memory at startup if latency spikes are unacceptable.
Set -XX:MaxGCPauseMillis to bound pause times, but beware of potential degradation if set too low.
Include Netty‑specific flags for off‑heap memory handling, e.g., -Dio.netty.tryReflectionSetAccessible=true with appropriate --add-opens options.
Conclusion : The high memory utilization was primarily due to the combination of a large heap size and G1GC’s region‑based allocation, which fully touches the heap and increases remembered‑set overhead. Reducing the heap and fine‑tuning G1 parameters restores stable memory usage and improves latency.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.