Root Cause Analysis and Optimization of G1GC Pauses Caused by StringTable Growth in Java Services
The investigation revealed that G1GC pauses were caused by uncontrolled growth of the JVM StringTable, triggered by Jackson’s InternCache interning countless random JSON field names, and was mitigated by enlarging the young generation, disabling field‑name interning, tuning G1 parameters, and monitoring StringTable size.
Problem description
Several strange phenomena were observed in a production service:
Even after setting a thread timeout, upstream still reported timeout exceptions.
The downstream responded quickly, but the overall request latency was extremely long.
GC pause time grew regularly and was not proportional to traffic.
Eden space suddenly decreased and stayed low for a long period.
These symptoms indicated that the young generation (young GC) was becoming a bottleneck.
Background
During a quiet afternoon a GC‑time alarm triggered. The service ran in a single container, and most GC pauses approached 1 s. After pulling the container out of load, the following observations were made:
No Full GC.
Young generation frequency and pause time increased dramatically.
Traffic remained stable.
The service used G1GC with a maximum pause target of 200 ms.
Initial hypothesis – increase young generation size
By default the young generation occupies 1/3 of the heap. The container was configured with a 10 GB heap but no explicit Eden size. Increasing the young generation space was expected to reduce GC frequency.
GC log analysis
[Parallel Time:
206.4
ms, GC Workers:
8
]
[GC Worker Start (ms): Min:
3867072696.3
, Avg:
3867072696.3
, Max:
3867072696.4
, Diff:
0.1
]
[Eden:
6120.0
M(
6120.0
M)->
0.0
B(
1356.0
M) Survivors:
24.0
M->
24.0
M Heap:
8625.7
M(
10.0
G)->
2506.0
M(
10.0
G)]The log shows a dramatic shrink of Eden, which explains the surge in young GC frequency.
Further investigation – StringTable overhead
After enabling -XX:G1LogLevel=finest the following root‑scanning times were observed:
StringTable Roots (ms): 187.6 189.9 189.8 188.5 190.1 189.7 189.8 189.8StringTable scanning alone consumed nearly the whole 200 ms pause budget, indicating that a large StringTable was the main culprit.
What is StringTable?
StringTable is a JVM‑level cache for interned strings. When String.intern() is called, the JVM checks this table; if the string is already present it is reused, otherwise it is added. Excessive interning of unique keys (e.g., random UUID‑like keys in JSON responses) can cause the table to grow without bound.
Root cause – Jackson InternCache
package com.fasterxml.jackson.core.util;
public final class InternCache extends ConcurrentHashMap<String, String> {
public String intern(String input) {
String result = get(input);
if (result != null) { return result; }
if (size() >= MAX_ENTRIES) { synchronized (lock) { if (size() >= MAX_ENTRIES) { clear(); } } }
result = input.intern();
put(result, result);
return result;
}
}Jackson’s serializer uses this cache for JSON field names. In the service, downstream responses contain a map with many random keys, causing the cache to fill the StringTable.
Related issue – fastjson
Older versions of fastjson also interned every JSON key, leading to the same problem. The issue was fixed in 1.1.24 by limiting the symbol table size.
Additional factor – ClassLoader roots
Extensive dynamic class loading adds entries to the JVM’s SystemDictionary , increasing the Ext Root Scanning time. A benchmark showed that heavy class loading can raise this phase to tens of milliseconds.
Solutions implemented
Increase the minimum young generation size: set -XX:G1NewSizePercent=5%→60% which reduced both GC frequency and pause time.
Disable interning for the problematic JSON keys: For Jackson, replace the default InternCache with a custom implementation that can be toggled via a system property ( -DonOff=true ). For fastjson, upgrade to ≥1.1.24 or disable JsonFactory.Feature.INTERN_FIELD_NAMES .
Adjust G1 parameters to avoid mixed GC dead‑locks: increase -XX:InitiatingHeapOccupancyPercent and -XX:G1HeapWastePercent as needed.
Monitor StringTable size using sun.jvm.hotspot.memory.StringTable or VisualVM OQL queries.
If possible, change the downstream contract to use stable keys instead of random ones, or move the random part to the value side of the map.
Final recommendations
When using G1GC, keep an eye on Ext Root Scanning (StringTable) in GC logs; spikes often indicate excessive interning.
Disable field‑name interning in JSON libraries for services that handle large amounts of dynamic keys.
Consider increasing young generation size or adjusting G1NewSizePercent to give the collector more breathing room.
Use class‑loader isolation wisely; unload unused classes to keep SystemDictionary growth in check.
By applying the above steps the service reduced GC pause times by roughly 25 % and eliminated the “young GC timeout” alarms.
HelloTech
Official Hello technology account, sharing tech insights and developments.
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.