47 Why Is It Necessary to Call Remove After Using Thread Local to Prevent Memory Leaks

47 Why is it Necessary to Call remove After Using ThreadLocal to Prevent Memory Leaks #

In this lesson, we will mainly explain why we need to call the remove method after using ThreadLocal.

First of all, we need to understand that this is related to memory leaks, so let’s first take a look at what a memory leak is.

What is a Memory Leak? #

A memory leak occurs when an object is no longer useful, but the memory it occupies cannot be reclaimed. In other words, the memory is leaked.

Under normal circumstances, if an object is no longer useful, the garbage collector (GC) should reclaim the memory occupied by it. This would allow the reclaimed memory to be reallocated elsewhere. However, if an object is not used but cannot be reclaimed, these unused objects will accumulate over time. This accumulation will result in less available memory, and eventually, it may lead to an Out of Memory (OOM) error.

Next, let’s analyze how memory leaks can occur in ThreadLocal.

Key Leak #

In the previous lesson, we analyzed the internal structure of ThreadLocal and learned that each Thread has a ThreadLocal.ThreadLocalMap variable named “threadLocals.” When a thread accesses ThreadLocal, it maintains the mapping between the ThreadLocal variable and the actual instance in the Entry of its ThreadLocalMap.

In our business code, we may perform the operation ThreadLocal instance = null in an attempt to clean up the ThreadLocal instance. However, suppose we have a strong reference to the ThreadLocal instance in the Entry of the ThreadLocalMap. In that case, even if the ThreadLocal instance is set to null in the business code, there is still a reference chain to the ThreadLocal instance in the Thread class.

During garbage collection, the GC performs reachability analysis. It will find that the ThreadLocal object is still reachable. Therefore, the ThreadLocal object will not be garbage collected, resulting in a memory leak.

The developers of the JDK considered this issue and made the Entry in ThreadLocalMap extend WeakReference, as shown in the following code:

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

    Object value;

    Entry(ThreadLocal<?> k, Object v) {

        super(k);

        value = v;

    }

}

As you can see, the Entry extends WeakReference. The characteristic of a weak reference is that if an object is only weakly referenced and has no strong references, the object can be reclaimed. Therefore, weak references do not prevent GC. This mechanism avoids the memory leak problem in ThreadLocal.

This is why the key in the Entry needs to use a weak reference.

Value Leak #

However, if we continue to study, we will find that although each Entry in ThreadLocalMap is a weak reference to the key, the Entry contains a strong reference to the value. Here is the code again:

static class Entry extends WeakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

    Object value;

    Entry(ThreadLocal<?> k, Object v) {

        super(k);

        value = v;

    }

}

As you can see, the line value = v represents a strong reference.

Under normal circumstances, when a thread terminates, the value associated with the key can be garbage collected because there are no more strong references to it. However, sometimes a thread’s lifecycle is long. If the thread does not terminate for a long time, the ThreadLocal and its associated value may no longer be useful. In this case, we need to ensure that both the ThreadLocal and the value can be reclaimed.

To further analyze this problem, let’s take a look at the specific reference chain with this diagram (solid lines represent strong references, dotted lines represent weak references):

img

As you can see, the left side shows the reference stack, with a reference to ThreadLocal and a reference to the thread. The right side shows our heap with object instances.

Let’s focus on this chain: Thread Ref → Current Thread → ThreadLocalMap → Entry → Value → Potentially leaked value instance.

This chain exists as long as the thread exists. If the thread performs a long-running task without terminating, when the garbage collector performs reachability analysis, the value will be reachable and not garbage collected. However, at the same time, we may have completed our business logic and no longer need this value. This results in a memory leak.

The JDK also considered this situation. When executing methods such as set, remove, and rehash on ThreadLocal, it scans the Entry with a null key. If it finds an Entry with a null key, it means that the corresponding value is no longer useful. Therefore, it sets the corresponding value to null. This allows the value object to be garbage collected normally.

However, suppose the ThreadLocal is no longer used. In that case, the set, remove, and rehash methods will not be called. At the same time, if the thread continues to live and does not terminate, the previously mentioned reference chain will continue to exist, leading to a memory leak of the value.

How to Avoid Memory Leaks #

After analyzing this problem, how can we solve it? The solution is to call the remove method of ThreadLocal. Calling this method can remove the corresponding value object and avoid memory leaks.

Let’s take a look at the source code of the remove method:

public void remove() {

    ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null)

        m.remove(this);

}

As you can see, it first gets a reference to the ThreadLocalMap and calls its remove method. This remove method can clear the value associated with the key, allowing the value object to be garbage collected.

Therefore, after using ThreadLocal, we should manually call its remove method to prevent memory leaks.