Yao

V1

2022/12/05阅读:21主题:兰青

ThreadLocal 内存泄露问题

内存泄漏(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
ThreadLocal

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

当 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()方法时,会进行清理

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

2、 在在set()方法最后调用了cleanSomeSlots()中还会有清理的操作。看一看cleanSomeSlots()

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

  1. 在调用get()方法时,如果没有命中,会向后查找,也会进行清理操作
  1. 调用remove()时,除了清理当前节点,还会向后进行清理操作

分类:

后端

标签:

Java

作者介绍

Yao
V1