Midnight Memory Leak: How Forgetting ThreadLocal.remove() Nearly Crashed Our Server
At 3 am a production server saw Full GC spike from once a day to ten times a minute because a ThreadLocal holding a large object was never removed, leading to a memory leak that almost took the machine down, and the article explains the root cause, internal mechanics, and remediation strategies.
At 3 am the service experienced a sudden surge in Full GC frequency—from once a day to ten times per minute—after a ThreadLocal holding a large object was not cleared, nearly causing the server to crash.
What is ThreadLocal
ThreadLocal can be thought of as a personal drawer for each thread; only the owning thread can see its contents. Internally each ThreadLocal has a ThreadLocalMap that stores entries where the key is the ThreadLocal object (a weak reference) and the value is the stored object (a strong reference).
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();Typical usage:
private static final ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
...
threadLocal.set(bigObject);
Thread.sleep(100); // simulate business logicWhy it leaks
The entry’s key is a weak reference, so when no external strong reference points to the ThreadLocal, the GC clears the key. The value, however, is a strong reference and remains attached to the thread.
key is a weak reference; GC can collect it.
value is a strong reference; stays alive.
In a thread‑pool scenario the thread lives for a long time. If a request stores a 100 MB intermediate result in ThreadLocal and finishes without calling remove(), the ThreadLocal object becomes unreachable, the key is collected, but the value stays in the thread’s ThreadLocalMap. The lingering value continuously consumes memory, eventually triggering OOM and alerting the system.
How to fix
Manual remove() – invoke threadLocal.remove() after use. Clean and has no side effects, but easy to forget.
try‑finally – wrap the usage in a try block and call remove() in finally. Guarantees execution, though adds a bit of boilerplate.
TransmittableThreadLocal – Alibaba’s open‑source enhanced ThreadLocal that automatically clears entries when threads return to the pool. Provides one‑stop cleanup for thread‑pool reuse, but introduces an additional dependency and is mainly aimed at async/cross‑thread context propagation.
Interview answer
Encountered it. The root cause is ThreadLocalMap’s Entry: the key is a weak reference, the value a strong reference. When the external strong reference disappears, the key is GC‑ed while the value remains attached to the thread. In a thread‑pool reuse scenario this leads to a memory leak. Fixes include using try‑finally to guarantee remove() , enforcing a policy that every set must be paired with a remove , or adopting TransmittableThreadLocal for automatic cleanup. Code reviews should add ThreadLocal usage to the checklist.
One‑line takeaway
ThreadLocal provides lock‑free thread‑local storage, but forgetting to call remove() turns it into a hidden memory bomb, similar to unsafe SQL concatenation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
