先上一段代码
运行代码:
这个实例可以很好的演示了:线程池中的每一个线程使用完 ThreadLocal 对象之后,再也不用,由于线程池中的线程不会退出,线程池中的线程的存在,同时 ThreadLocal 变量也会存在,占用内存!造成 OOM 溢出!
那么ThreadLocal为什么能造成OOM溢出呢?
1、先来看一下 ThreadLocal 的原理图
ThreadLocal、Thread、ThreadLocalMap、Entry 之间的关系:
由上图可以看出:
- 一个Thread对应着一个ThreadLocalMap
- 一个ThreadMap却对应着多个ThreadLocal
- 一个ThreadLocal包含多个Entry
在 ThreadLocal 的生命周期中,都存在这些引用。看下图:实线代表强引用,虚线代表弱引用。
2、ThreadLocal 的实现: 即就是每个 Thread 维护一个 ThreadLocalMap
映射表,这个映射表的 key 是 ThreadLocal
实例本身,value 是真正需要存储的 Object。
3、也就是说 ThreadLocal
本身并不存储值,它只是作为一个 key 来让线程从ThreadLocalMap
获取 value。值得注意的是图中的虚线,表示ThreadLocalMap
是使用 ThreadLocal
的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
4、ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
5、总的来说就是,ThreadLocal 里面使用了一个存在弱引用的 map,map 的类型是ThreadLocal.ThreadLocalMap.
Map中的 key 为一个 threadlocal 实例。这个 Map 的确使用了弱引用,不过弱引用只是针对 key。每个 key 都弱引用指向 threadlocal。 当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。
但是,我们的 value 却不能回收,而这块 value 永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread
连接过来的强引用。只有当前thread结束以后,current thread
就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收。最好的做法是将调用 threadlocal 的 remove 方法,这也是等会后边要说的。
6、其实,ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的get(),set(),remove()
的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value。这一点在上一节中也讲到过!
7、但是这些被动的预防措施并不能保证不会内存泄漏:
(1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。
(2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。
为什么要使用弱引用?
上面导致内存溢出到底是不是这个弱引用导致的呢?
1、从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析 ThreadLocal 使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
(1)key 使用强引用:引用的ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致 Entry 内存泄漏。
(2)key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set、get、remove
的时候会被清除。
由于ThreadLocalMap
的生命周期跟 Thread
一样长,如果都没有手动删除对应 key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的 value
在下一次ThreadLocalMap
调用set、get、remove
的时候会被清除。
ThreadLocal
内存泄漏的根源:
由于 ThreadLocalMap
的生命周期跟 Thread
一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。