Diagnosing G1 Young GC Slowdown Caused by Excessive XStream ClassLoader Creation
The investigation revealed that repeatedly creating XStream objects generated thousands of CompositeClassLoader instances, inflating the JVM’s SystemDictionary and causing extremely long Ext Root Scanning during G1 Young GC pauses, which was resolved by caching XStream per class and reducing YGC pauses from seconds to milliseconds.
A user reported intermittent server stalls and long response times when clicking a query button, suspecting MySQL latency. Monitoring showed increasing MySQL connection times, but further investigation on Alibaba Cloud RDS indicated low database load.
The real bottleneck was identified during on‑site troubleshooting: JVM pause durations (STW) were dominated by G1 Young GC (YGC) pauses. Analysis of GC logs revealed that Ext Root Scanning consumed up to 596 ms of a 457 ms YGC, indicating a root‑scanning issue.
Deep dive into the heap dump highlighted an abnormal number of CompositeClassLoader instances created by XStream. Each XStream instance creates a new CompositeClassLoader , which in turn loads many classes, inflating the SystemDictionary::dictionary() with millions of entries. These entries become roots during YGC, causing extensive Ext Root Scanning.
To reproduce the problem, a series of demos were built:
Demo 1 creates an XStream instance in an infinite loop:
public static void main(String[] args) {
while (true) {
new XStream();
}
}Running with G1GC options (e.g., -XX:+UseG1GC -XX:+PrintGCDetails -XX:+UnlockExperimentalVMOptions -Xms2g -Xmx2g -XX:G1NewSizePercent=40 ) reproduces the YGC slowdown.
Subsequent demos refined the workload to isolate the cause, eventually showing that repeatedly creating CompositeClassLoader and loading classes via Class.forName(..., true, compositeClassLoader) triggers the issue.
The fix involved caching XStream instances per class type, preventing redundant CompositeClassLoader creation:
/**
* Reuse XStream per class
*/
public static XStream getInstance(Class
clazz, String driverType) {
return allXStream.computeIfAbsent(clazz, k -> createNew(driverType));
}After deploying the cache, YGC pause times dropped from ~1.5 s to ~10 ms, eliminating the performance problem.
Further investigation into HotSpot source code explained why the root‑scanning time grew: each new class loader adds entries to the global SystemDictionary , which are scanned during every YGC. The dictionary is not cleared until a full GC, so the growing number of entries dramatically increases Ext Root Scanning duration.
The article concludes with a summary of class‑loading concepts, G1 GC behavior, and best practices for using XStream (e.g., singleton usage) to avoid similar pitfalls.
DeWu Technology
A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.
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.