A Memory Leak refers to a program that fails to release or release dynamically allocated heap Memory for some reason, resulting in a waste of system Memory, slowing down the program and even crashing the system. — Baidu Encyclopedia

In Java, this means that there are objects that no longer have any references, but the GC cannot reclaim the memory in which the object is located, so there is a memory leak.

ThreadLocal addresses the problem that objects cannot be accessed by multiple threads at the same time. See the source code for ThreadLocal to see how it is implemented.

ThreadLocal sets the set() method of the data

  public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if(map ! =null)
            map.set(this, value);
        else
            createMap(t, value);
    }
​
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
​
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
Copy the code

If you are using ThreadLocal to set data, you are actually setting it to the threadLocals field of the current Thread. Check out the threadLocals variable

  ThreadLocal.ThreadLocalMap threadLocals = null;
Copy the code

ThreadLocals is the internal class ThreadLocalMap of ThreadLocal, which is used to store data in an internal class called Entry

 static class Entry extends WeakReference<ThreadLocal<? >>{ Object value; Entry(ThreadLocal<? > k, Object v) {super(k); value = v; }}Copy the code

The key of the Entry is the current ThreadLocal, and the value is the data we want to set.

WeakReference represents a WeakReference. When the JVM GC, once it finds an object with only weak references, its memory will be reclaimed regardless of whether the current memory space is sufficient.

Because the WeakReference < ThreadLocal
>, so in Entry ThreadLocal is a weak reference. Once GC occurs, ThreadLocal will be reclaimed by GC, but value is a strong reference and will not be reclaimed. Let me draw a picture of it

In the figure, solid lines represent strong references and dotted lines represent weak references.

When a JVM GC occurs, the dashed line breaks, the key is null, the value is a strong reference is not null, the entire Entry is not null, it is still in ThreadLocalMap and occupies memory.

When we get data, we use the get() method of ThreadLocal. ThreadLocal is not null, so we cannot access the entry value with a null key. This creates a memory leak.

Since weak references cause memory leaks, why not use strong references?

The answer is no. If it is a strong reference, look at the following code

 ThreadLocal threadLocal = new ThreadLocal();
 threadLocal.set(new Object());
 threadLocal = null;
Copy the code

After setting the data, we directly set threadLocal to null. In this case, the threadLocal Ref in the stack is disconnected from the threadLocal in the heap, but the reference from the key to threadLocal still exists, and the GC still cannot reclaim it, which will also cause a memory leak.

So what’s a weak quote better than a strong quote?

When a key is a weak reference and threadLocal is set to null, the threadLocal Ref on the stack breaks with ThreadLoacl on the heap, and the key to ThreadLoacl breaks with GC, then the threadLocal can be reclaimed.

ThreadLocal can also use key.get() == null to determine if the key has been collected, so ThreadLocal can clean up the stale nodes itself to avoid memory leaks.

In fact, ThreadLocal does a great job of cleaning up expired keys to avoid memory leaks

  1. When the set () method is called, it is cleaned up

     private void set(ThreadLocal
              key, Object value) {
    ​
         Entry[] tab = table;
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1);
    ​
         for(Entry e = tab[i]; e ! =null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<? > k = e.get();if (k == key) {
                  e.value = value;
                  return;
               }
                // If the key is null, replace it
               if (k == null) {
                   replaceStaleEntry(key, value, i);
                   return;
               }
         }
    ​
         tab[i] = new Entry(key, value);
         int sz = ++size;
         // Clear some slots, and clear expired keys
         if(! cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }Copy the code

    1. If the key is null, the current location will be overwritten by the GC.

    2. There is also a cleanup in the set() method that calls cleanSomeSlots() at the end. Take a look at the cleanSomeSlots ()

     private boolean cleanSomeSlots(int i, int n) {
         boolean removed = false;
         Entry[] tab = table;
         int len = tab.length;
         do {
             i = nextIndex(i, len);
             Entry e = tab[i];
             if(e ! =null && e.get() == null) {
                 n = len;
                 removed = true;
                 // Real cleanupi = expungeStaleEntry(i); }}while ( (n >>>= 1) != 0);
          return removed;
     }
    ​
    Copy the code

    CleanSomeSlots () when judging e! = null && LLDB () == null is true. ExpungeStaleEntry () will be called to clean up.

  2. When the get() method is called, if there is no hit, it is looked back and cleaned up as well

    private Entry getEntry(ThreadLocal
              key) {
         int i = key.threadLocalHashCode & (table.length - 1);
         Entry e = table[i];
         if(e ! =null && e.get() == key)
             return e;
         else
             // There was no backward lookup hit
            return getEntryAfterMiss(key, i, e);
     }
     private Entry getEntryAfterMiss(ThreadLocal<? > key,int i, Entry e) {
         Entry[] tab = table;
         int len = tab.length;
    ​
         while(e ! =null) { ThreadLocal<? > k = e.get();if (k == key)
                 return e;
             if (k == null)
                 // If the key is null, it is collected by the GC
                 expungeStaleEntry(i);
             else
                 i = nextIndex(i, len);
             e = tab[i];
         }
         return null;
     }
    Copy the code
  3. When you call remove(), in addition to cleaning up the current node, you also clean up backwards

     private void remove(ThreadLocal
              key) {
         Entry[] tab = table;
         int len = tab.length;
         int i = key.threadLocalHashCode & (len-1);
         for(Entry e = tab[i]; e ! =null;
              e = tab[i = nextIndex(i, len)]) {
              if (e.get() == key) {
                  e.clear();
                  // Go back and clean up
                  expungeStaleEntry(i);
                  return; }}}Copy the code