ThreadLocal内存溢出(OOM)的原因

ThreadLocal是Java提供的一个线程安全类,其原理是每个线程都拥有各自的变量内存副本。其实就是每个线程Thread里都有一个ThreadLocalMap类,用于存储变量值。更新、删除操作时,都是操作各自线程里的hreadLocalMap类,互不影响,从而达到的线程安全

ThreadLocal经常用于一次调用的上下文储存场景,例如一次调用的token、traceId,在调用的各个阶段都有可能用到,但是每次调用的值都不一样,这时ThreadLocal就派上用场了

但是如果ThreadLocal使用不当,会引发内存溢出,其实ThreadLocal的作者Josh Bloch在写ThreadLocal的时候,意识到了这点,例如弱引用等,但是依然会有内存溢出的可能,下面我们来分析一下原因

看源码ThreadLocal底层都是用的Thread里的ThreadLocalMap类,其实ThreadLocal就是一个工具类而已,底层都是操作的Thread里的ThreadLocalMap类。

再看看ThreadLocalMap的源码

当我们使用ThreadLocal.set()时,set的value与key(即业务自己定义的ThreadLocal类)会存储在ThreadLocalMap的Entry[]数组里

其中Entry是实现了一个弱引用WeakReference,Entry的key(即业务方定义的 ThreadLocal类)会被包装成一个弱引用当成Entry的key。Java的弱引用的定义是,当JVM执行垃圾回收扫描的时候,当发现只有弱引用的对象时,会立即回收此对象,这是ThreadLocal当初设计的时候防止内存溢出的一个手段

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

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

虽然key被包装成了一个弱引用会被垃圾回收机制给回收,但是value在线程(Thread)不死亡时却可能存在一条强引用链.

由于value是强引用,只要Thread不死亡时,例如线程池,这条强引用链就会存在,那么value就不会回收,可能造成内存溢出

虽然ThreadLocal的作者想到了这点,也做了些优化,例如在get的时候当发现key是null的时候,会遍历一次整个Entry数组,remove掉key为null的entry,把value指向null,消除这条强引用链。源码方法为expungeStaleEntry

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

        // expunge entry at staleSlot
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal k = e.get();
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;

                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

但是这个消除强引用链的动作是需要业务方在get的情况下触发的,可能业务方并不会get、也可能get是key不为空,并不会触发expungeStaleEntry类。所以开发者要养成良好的习惯,记得用完ThreadLocal时,调一次ThreadLocal.remove()方法或者ThreadLocal.set(null)

你可能感兴趣的:(java相关,java)