Skip to content

ClassLoader leak / Metaspace OOM caused by static ThreadLocal in DataRecord #41

@QiuYucheng2003

Description

@QiuYucheng2003

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:

  1. The container's worker thread holds a reference to the ThreadLocalMap.

  2. The Map Entry holds the DecimalFormat instance (Value).

  3. The DecimalFormat instance holds a reference to its Class/ClassLoader (WebAppClassLoader).

  4. 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

  1. Include SeisFile library in a WAR/Web Application.

  2. Deploy to a container (e.g., Tomcat) with a thread pool.

  3. Invoke logic that calls DataRecord.oneLineSummary() (triggers decimalFormat.get()).

  4. Undeploy and redeploy the application multiple times.

  5. 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;

}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions