-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Describe the bug
I identified a ClassLoader leak issue in edu.sc.seis.seisFile.mseed.DataRecord.
The class uses a private static final ThreadLocal decimalFormat to cache formatter instances. However, there is no mechanism to invoke remove() on this ThreadLocal.
In container environments (like Tomcat/Jetty) where threads are pooled and long-lived:
-
The container's worker thread holds a reference to the ThreadLocalMap.
-
The Map Entry holds the DecimalFormat instance (Value).
-
The DecimalFormat instance holds a reference to its Class/ClassLoader (WebAppClassLoader).
-
Since the ThreadLocal is static and never cleaned up, the ClassLoader cannot be garbage collected upon application reload/undeploy.
Impact
This leads to java.lang.OutOfMemoryError: Metaspace after multiple application redeployments, crashing the underlying physical server or container.
Source Code Location:
// DataRecord.java (approx. Line 439)
private static final ThreadLocal decimalFormat = new ThreadLocal() {
@OverRide
protected DecimalFormat initialValue() {
return (new DecimalFormat("#####.####", new DecimalFormatSymbols(Locale.US)));
}
};
Steps to Reproduce
-
Include SeisFile library in a WAR/Web Application.
-
Deploy to a container (e.g., Tomcat) with a thread pool.
-
Invoke logic that calls DataRecord.oneLineSummary() (triggers decimalFormat.get()).
-
Undeploy and redeploy the application multiple times.
-
Observation: The WebAppClassLoader instances are not collected (visible in Heap Dump), eventually filling Metaspace.
Expected behavior
Libraries should avoid using static ThreadLocal for object caching without providing a cleanup hook. Given DecimalFormat creation cost is negligible on modern JVMs, the ThreadLocal cache should be removed.
Suggested Fix
Remove the ThreadLocal and instantiate DecimalFormat as a local variable or instance variable when needed
Proposed Change:
// Remove static ThreadLocal
// private static final ThreadLocal decimalFormat = ...
public String oneLineSummary() {
// Create local instance (safe and lightweight)
DecimalFormat df = new DecimalFormat("#####.####", new DecimalFormatSymbols(Locale.US));
String s = getHeader().getTypeCode()+" "+ getHeader().getCodes() + " "
+ getStartTime() + " " + df.format(getHeader().getNumSamples()/ getSampleRate() ) +" "+getHeader().getNumSamples();
return s;
}