当前位置:   article > 正文

ThreadLocal内存泄漏问题和解决方法

threadlocal内存泄漏问题

一、问题分析

测试用例

	Thread thread = Thread.currentThread();
    ThreadLocal<Integer> tl = new ThreadLocal<>();
    tl.set(1);
    System.gc(); // 手动提醒下gc
    Thread.sleep(100); // 让gc先执行
    System.out.println(111);
    tl = null; // 断开ThreadLocal引用
    System.gc();
    Thread.sleep(100);
    System.out.println(111); // 断点打这里
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述
可以看到线程中的threadLocals还持有这个ThreadLocalMap的引用,其中Entry中value还是有值的为1,但是referent为null,表示已经被GC
这里可以看Entry的实现

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

public abstract class Reference<T> {

	private T referent;         /* Treated specially by GC */

	Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

简单来说:ThreadLocalMap还持有Entry对象的引用,所以Entry对象并不会被释放。而由于key是WeakReference中的referent,对于Entry->referent这个关联引用会在gc的时候断开,所以可以看到上面的referent为null,但是ThreadLocalMap中还存在entry对象。所以这样value是会一直被引用的,可能会导致内存溢出的问题存在。

二、如何避免

我们可以使用 remove来避免这种情况

	Thread thread = Thread.currentThread();
    ThreadLocal<Integer> tl = new ThreadLocal<>();
    tl.set(1);
    System.gc(); // 手动提醒下gc
    Thread.sleep(100); // 让gc先执行
    System.out.println(111);
    tl.remove(); // 调用remove
    tl = null; // 断开ThreadLocal引用
    System.gc();
    Thread.sleep(100);
    System.out.println(111); // 断点打这里
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述
这里table里面只有3个了,说明已经被回收了。

ThreadLocal.remove()方法其实就是获取当前线程的threadLocals(ThreadLocalMap)并调用其remove()方法
ps: 可以对照set方法看看实现,这里解决hash冲突的话不是和HashMap一样用拉链法,而是去找下一个没有设置值得结点

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) { // 找到对应的ThreadLocal
            e.clear(); // this.referent = null; 断开引用
            expungeStaleEntry(i); 
            return;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

继续看 expungeStaleEntry 的实现

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 将Entry的value也就是ThrealLocal对应的值置为null
    // 断开tab是对应槽位和Entry的关联
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 下面是rehash操作不是本次重点
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // ......
    }
    return i;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/240404
推荐阅读
相关标签
  

闽ICP备14008679号