ThreadLocal内存泄漏分析
使用场景
-
在我们想让线程间数据不共享,比如:数据库连接、Session 管理,会想到ThreadLocal可以实现我们需求。但是使用不当会出现内存泄露情况。
本地模拟情况
-
代码如下
static class LocalVariable { byte[] s = new byte[1024*1024 * 1000]; } static ThreadLocal<LocalVariable> local = new ThreadLocal<>(); public static void main(String[] args) throws IOException { local.set(new LocalVariable()); // local.remove(); System.in.read(); }
当我们执行一次GC出现如下界面
-
当加上remove方法后的代码如下
static class LocalVariable { byte[] s = new byte[1024*1024 * 1000]; } static ThreadLocal<LocalVariable> local = new ThreadLocal<>(); public static void main(String[] args) throws IOException { local.set(new LocalVariable()); local.remove(); System.in.read(); }
当我们也执行一次GC出现如下界面
-
总结:当我们主动调用remove()方法后执行GC的时候会将我们的内存释放掉。
原因分析
-
看下面图片 我们会发现key是一个弱引用,而值是一个强应用。为啥key是弱引用的情况下,GC回收不了呢?带着问题我们跟下源码
源码分析
调用set方法执行如何代码
/** * Sets the current threads copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current threads copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
可以看出当当前线程作为key生成ThreadLocalMap对象,然后设置value。进入getMap后源码如下
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
返回当前线程持有的ThreadLocalMap,ThreadLocalMap源码如下 我们看到ThreadLocalMap只是针对key做了弱引用,value仍然被存活的线程持有。在线程还存活的情况下GC是无法回收value的内存的。
-
为啥我们调用remove后就可以释放内存呢?remove执行代码如下
/** * Removes the current threads value for this thread-local * variable. If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim. This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
我们重点看下 m.remove(this)
/** * Remove the entry for key. */ 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) { e.clear(); expungeStaleEntry(i); return; } } }
我们可以看到调用了e.clear();来清除Entry。所以在执行GC后会释放内存。
为啥key要设置成弱引用
在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。会多一层保障