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<ThreadLocal<?>> {
            /** 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)

猜你喜欢

转载自blog.csdn.net/CSDNzhangtao5/article/details/103228399