0 写在前边
今天以 “TheadLocal 为什么会导致内存泄漏” 为题与朋友们讨论了一波,引出了一些原理性的内容,本文就这个问题作答,并扩展相关的知识点
1 ThreadLocal 和 ThreadLocalMap 是什么?
简单来说,ThreadLocal 是一种操作与线程绑定的共享对象的工具,通过ThreadLocal可以将一些对象保存在线程上,实现同线程不同方法之间的对象共享。
线程的上下文由 ThreadLocalMap 组成,它是 ThreadLocal 的静态内部类,存储着线程共享对象。
一般来说,我们无需显式创建ThreadLocalMap,也无需为装入ThreadLocalMap 对象设 key 值,因为在 set 方法执行时会创建 ThreadLocalMap,并将当前 ThreadLocal 对象作为 key,待存储对象作为 value,存储到 ThreadLocalMap。
值得一提的是,ThreadLocalMap 的 key 与 value 的类型是不同的,key 是弱引用类型的,value 是强引用类型的。
2 Thread、ThreadLocal 与 ThreadLocalMap 之间的关系
Thread 与 ThreadLocalMap
首先 ThreadLocalMap 是与 Thread 进行绑定的,ThreadLocalMap 是线程上实际存储共享对象的容器。
如下图,threadLocals
就是默认的 ThreadLocalMap,默认为 null
绑定 ThreadLocalMap 到 Thread 的位置在 ThreadLocal 的 createMap
方法中,threadLocals
引用指向 ThreadLocalMap。(这里还包含了放置第一个对象的操作)
ThreadLocal 的 getMap
方法取的就是线程的 threadLocals
ThreadLocal与ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 类的静态内部类,ThreadLocal 是操作 ThreadLocalMap 的工具,还是 ThreadLocalMap 的 key 对象,在 ThreadLocal 作为 key 保存前转换成弱引用类型。
一般我们通过 ThreadLocal 的 set 方法进行保存对象,在 set
方法内部获取了当前线程的 ThreadLocalMap,调用 ThreadLocalMap 的 set
方法进行保存对象。
使用 this 关健字将当前使用的 ThreadLocal 对象作为 key 存到 ThreadLocalMap 中,以减小 key 冲突的可能性。
ThreadLocalMap 中的 set 方法主要是创建一个 Entry 对象放进数组中,Entry 继承 WeakReference 类,将 Entry 的 key(也就是 ThreadLocal)转成弱类型。
一句话总结它们之间的关系
每个 Thread 绑定 ThreadLocalMap 来存储线程上下文共享对象,ThreadLocalMap 中的key(即,ThreadLocal)在同一线程中是唯一的。单线程情况下,每个 ThreadLocal 只对应一个值对象。
3 ThreadLocal导致的内存泄漏的原因是什么?
导致内存泄漏的原因在于程序员未在使用完ThreadLocalMap中存储的对象后清除这些对象。
ThreadLocalMap是维护在Thread内部的,意味着只要线程不退出,ThreadLocalMap中保存的对象引用就会一直存在,由于垃圾回收器是依据可达性分析的,存在强引用的对象不会被回收,而ThreadLocalMap中存储的对象都是强引用的。
假设当前线程处于一个死循环中(比如,Tomcat),随着ThreadLocalMap保存的对象越来越多,垃圾收集器无法回收强引用的对象,就会导致可用堆内存越来越小,出现内存泄漏,最终抛出OOM。
4 如何清理 ThreadLocalMap 存储的对象?
用完 ThreadLocal 存储的对象后,只需调用 ThreadLocal 的 remove 方法,就会自动将 ThreadLocalMap 中的 K-V 对引用置空,垃圾收集器会在合适的时机内清除 K-V 对象释放内存。
ThreadLocal 类 remove 方法,获取当前线程上的 ThreadLocalMap 移除以此 ThreadLocal 为 key 的对象。通过调用 ThreadLocalMap 的 remove 方法实现。
ThreadLocalMap 的 remove 方法中,e.clear()
调用的是key对象继承的 Reference 类的 clear()
,对 key 引用置空,expungeStaleEntry(i)
对 value 引用置空。
ThreadLocalMap 的 expungeStaleEntry
方法,分别取出 ThreadLocalMap 中的 Entry 的 value 与 Entry 本身先后置空。
5 为什么ThreadLocalMap使用弱引用key?
ThreadLocalMap 是与线程绑定的,线程不退出,强引用的key对象就不会被垃圾回收,当用户妥善处理的无用K-V对象就会导致内存泄漏。利用弱引用可以及时被 GC 的特性,回收绝大多数key(除 static 域的全局 key 外),以减缓内存泄漏。
实际上最需要回收的是value对象,弱引用key只是一种挽救措施。
6 ThreadLocalMap 为什么使用强引用 value,而不是弱引用?
与 key 不同的是,key 仅作为索引,实际工作的是 value,value 需要共享。
当局部 value 对象所在的方法结束,栈桢被清空时,会将局部 value 对象引用销毁,垃圾收集器会清除没有引用的对象。
如果此时设置成弱引用装入 Map,value 对象会在某次 GC 时消亡,这肯定不是我们希望的。
我们希望的是value对象可以维持存活以共享,只有强引用可以达到目的。
7 线程池会累积 ThreadLocalMap 的占用的内存而出现内存泄漏吗?
解释下问题,之前有讲过,ThreadLocalMap 与 Thread 的生命周期是一致的,而线程池技术是复用线程的,如果之前的 ThreadLocalMap 已经开始内存泄漏,是否会出现累积已泄漏的内存?
线程池不存在这个问题,虽然它复用了线程,但是清除了上一线程的所有资源。
8 线程有一个ThreadLocalMap,ThreadLocal也只有一个值,为何还会内存泄漏?
这是我自己思考时提出来的,能问出这个问题,只能说当时还没完全理解ThreadLocal与ThreadLocalMap的对应关系。
原问题:一个线程有一个ThreadLocalMap(不考虑继承ThreadLocal的那个实现),即然 ThreadLocal 作为 key 了,那么ThreadLocalMap中是否只会有一个Entry,内存再泄露能泄露到哪里去?(误认为ThreadLocalMap与ThreadLocal绑定,只有一个,也只能装一个Entry,这是错误的)
其实 ThreadLocal 我们可以创建很多个,ThreadLocalMap却只有一个(不考虑继承ThreadLocal的那个实现),通过创建多个 ThreadLocal 来存取 ThreadLocalMap 中的对象。
伪代码举例:
ThreadLocal<A> aThreadLocal = new ThreadLocal<A>();
ThreadLocal<B> bThreadLocal = new ThreadLocal<B>();
aThreadLocal.set(new A("a"));
bThreadLocal.set(new B("b"));
aThreadLocal.get();
bThreadLocal.get();
我在ThreadLocal的getMap()打了断点,当前线程中 ThreadLocalMap 中有两个对象,可以看到referent中记录了保存对象的ThreadLocal对象的HashCode。这起码证明了ThreadLocalMap不仅仅能装一个对象
9 【扩展】Java对象的引用类型
- 强引用:常见new的对象,只要还有强引用的对象,则不会被GC
- 软引用:比强引用弱,仅当JVM内存不足时才会清理,清理时机在OOM前
- 弱引用:只提供非强制的映射关系,会被JVM择机清理
- 虚引用(幻象引用):无法通过它访问对象,只确保对象在finalize后执行某些操作