ThreadLocal 内存泄露问题

1,089 阅读4分钟

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 ——百度百科

上述的意思用在java中就是存在已经没有任何引用的对象,但是GC又不能把对象所在的内存回收掉,所以就造成了内存泄漏。

ThreadLocal主要解决的是对象不能被多个线程同时访问的问题。根据ThreadLocal的源码看看它是怎么实现的。

ThreadLocal设置数据的set()方法

  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);
    }

可以看到在使用ThreadLocal设置数据时,其实设置到的是当前线程的threadLocals字段里,去Thread里看一看threadLocals变量

  ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals的类型是ThreadLocal里的内部类ThreadLocalMap,ThreadLocalMap的中用来存储数据的又是一个内部类是Entry

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

Entry的key是当前ThreadLocal,value值是我们要设置的数据。

WeakReference表示的是弱引用,当JVM进行GC时,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。

因为WeakReference<ThreadLocal<?>>,所以在EntryThreadLocal是弱引用,一旦发生GC,ThreadLocal会被GC回收掉,但是value是强引用,它不会被回收掉。用一张图来表示一下

ThreadLocal.drawio.png

图中实线表示的是强引用,虚线表示的是弱引用。

当JVM发生GC后,虚线会断开应用,也就是key会变为null,value是强引用不会为null,整个Entry也不为null,它依然在ThreadLocalMap中,并占据着内存,

我们获取数据时,使用ThreadLocal的get()方法,ThreadLocal并不为null,所以我们无法通过一个key为null去访问到该entry的value。这就造成了内存泄漏。

既然用弱引用会造成内存泄漏,直接用强引用可以么?

答案是不行。如果是强引用的话,看看下面代码

 ThreadLocal threadLocal = new ThreadLocal();
 threadLocal.set(new Object());
 threadLocal = null;

我们在设置完数据后,直接将threadLocal设为null,这时栈中ThreadLocal Ref 到堆中ThreadLocal断开了,但是keyThreadLocal的引用依然存在,GC依旧没法回收,同样会造成内存泄漏。

那弱引用比强引用好在哪?

当key为弱引用时,同样是上面代码,当threadLocal设为null时,栈中ThreadLocal Ref 到堆中ThreadLoacl断开了,keyThreadLoacl也因为GC断开了,这时ThreadLocal就可以被回收了。

同时,ThreadLocal也可以根据key.get() == null 来判断key是否已经被回收,因此ThreadLocal可以自己清理这些过期的节点来避免内存泄漏。

其实,ThreadLocal做了很大的工作清除过期的key来避免发生内存泄漏

  1. 在调用set()方法时,会进行清理

     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;
               }
                // 当key为null时,替换掉
               if (k == null) {
                   replaceStaleEntry(key, value, i);
                   return;
               }
         }
    ​
         tab[i] = new Entry(key, value);
         int sz = ++size;
         // 清理一些槽位,清理过期key
         if (!cleanSomeSlots(i, sz) && sz >= threshold)
             rehash();
     }
    ​
    

    1、 当key为null时,说明该位置被GC回收了,会将当前位置覆盖掉。

    2、 在set()方法最后调用了cleanSomeSlots()中还会有清理的操作。看一看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;
                 // 真正的清理工作
                 i = expungeStaleEntry(i);
              }
          } while ( (n >>>= 1) != 0);
          return removed;
     }
    ​
    

    cleanSomeSlots()中当判断e != null && e.get() == null为true时,说明已经被GC回收了,会调用expungeStaleEntry()进行清理工作,具体的逻辑就不再看了。

  2. 在调用get()方法时,如果没有命中,会向后查找,也会进行清理操作

    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
             // 没有命中向后查找
            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)
                 // 当key为null,说明被GC回收了,进行清理的操作
                 expungeStaleEntry(i);
             else
                 i = nextIndex(i, len);
             e = tab[i];
         }
         return null;
     }
    
  3. 调用remove()时,除了清理当前节点,还会向后进行清理操作

     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();
                  // 向后查找,进行清理操作
                  expungeStaleEntry(i);
                  return;
               }
          }
     }