原因分析
内存溢出的问题和解决方案比较简单,重点在于“原因分析”,我们要通过内存溢出的问题搞清楚,为什么 ThreadLocal 会这样?是什么原因导致了内存溢出?
要搞清楚这个问题(内存溢出的问题),我们需要从 ThreadLocal 源码入手,所以我们首先打开 set 方法的源码(在示例中使用到了 set 方法),如下所示:
public void set(T value) { // 得到当前线程 Thread t = Thread.currentThread(); // 根据线程获取到 ThreadMap 变量 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); // 将内容存储到 map 中 else createMap(t, value); // 创建 map 并将值存储到 map 中 }
从上述代码我们可以看出 Thread、ThreadLocalMap 和 set 方法之间的关系:每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中,所以接下来我们再看一下 ThreadLocalMap 的源码:
staticclass ThreadLocalMap { // 实际存储数据的数组 private Entry[] table; // 存数据的方法 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(); // 如果有对应的 key 直接更新 value 值 if (k == key) { e.value = value; return; } // 发现空位插入 value if (k == null) { replaceStaleEntry(key, value, i); return; } } // 新建一个 Entry 插入数组中 tab[i] = new Entry(key, value); int sz = ++size; // 判断是否需要进行扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } // ... 忽略其他源码 }
从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
根据上面的内容,我们可以得出 ThreadLocal 相关对象的关系图,如下所示:
也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。