ThreadLocal造成内存泄漏的原因

答案:

  • Entry[] table数组中有很多索引,指向堆空间 key为null、value不为null的entry对象。
  • 这些entry对象key为null时,就没法使用了,应该被回收。
  • 但是Entry[] table数组一直存在,有索引指向entry对象,导致entry对象无法删除。

分析:

1、什么是Entry[] table数组?

Entry[] table数组是ThreadLocalMap类的成员属性 private Entry[] table;

ThreadLocalMap类是 ThreadLocal 类的静态内部类。

Thread 类的成员变量 threadLocals 的类型是 ThreadLocal.ThreadLocalMap。

2、Entry类的对象存什么?

Entry类存一对键值对,key是 ThreadLocal 对象的弱引用,value是 Object对象的强引用。

  • 弱引用特点:只有弱引用指向的对象,jvm一触发垃圾回收,就会回收此对象。
  • 强引用特点:强引用指向的对象,jvm垃圾回收不会回收此对象。
  • 所以,只要jvm触发垃圾回收,就会存在key为null的entry对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

3、为什么Entry[] table数组一直存在?

开发中一般用线程池管理线程,核心线程会一直运行。那么核心线程的 threadLocals 属性指向的 ThreadLocalMap 对象会一直存在,ThreadLocalMap 底层的 Entry[] table 数组也会一直存在。

4、为什么 Entry[] table数组中索引指向的“key为 null 的 entry对象”不会删除?

直接看更新源码:

1、更新时,会先遍历 Entry[] 数组,判断key是否存在,存在则覆盖value。

  • 传入的key肯定不为null,key也就是ThreadLocal的对象,通过此对象调用set方法,那此对象肯定不为null。
  • 所以这边不会命中 key为null的Entry对象。

2、若Entry[] table数组 不存在此key的Entry对象,则创建新的entry对象插入Entry[] table数组。

所以,更新方法不会操作key为null的Entry对象。

//修改当前线程的 ThreadLocalMap
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(1);
//ThreadLocal 部分源码 
public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap对象
        if (map != null) {
            map.set(this, value);//存在则更新
        } else {
            createMap(t, value);//不存在,创建ThreadLocalMap对象,再更新
        }
    }



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)]) { //遍历Entry[]数组
                ThreadLocal<?> k = e.get();

                if (k == key) { //比较key是否存在,存在则覆盖value
                    e.value = value;
                    return;
                }

                if (k == null) { //这是针对内存泄漏的解决方法
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);//不存在,创建新的Entry对象添加到数组中
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold) //判断数组长度是否要扩容
                rehash();
        }

5、如何解决?

ThreadLocal 类的 set 方法中,判断key为null,则将Entry[] table数组对应的索引指向null。

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 (k == null) { //这是针对内存泄漏的解决方法
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


版权声明:本文为weixin_39466433原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。