问题:
1、ThreadLocal造成内存泄漏的原因? 2、ThreadLocal内存泄漏解决方案?
1. ThreadLocal造成内存泄漏的原因?
ThreadLocal的操作都是基于ThreadLocalMap展开的,而ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构。Entry用于保存ThreadLocalMap的Key-Value对条目,但是Entry使用了对ThreadLocal实例进行包装之后的弱引用(WeakReference)作为Key,其代码如下:
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
为什么Entry需要使用弱引用对Key进行包装,而不是直接使用ThreadLocal实例作为Key呢?
这里从一个简单的例子入手,假设有一个方法funcA()创建了一个“线程本地变量”,具体如下:
public void funA(){
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
threadLocal.set(100);
threadLocal.get();
}
当线程tn执行funcA()方法到其末尾时,线程tn相关的JVM栈内存以及内部ThreadLocalMap成员的结构大致如图: 当线程tn执行完funcA()方法后,funcA()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向ThreadLocal实例。如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏问题。 什么是弱引用呢?
仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。
什么是内存泄漏?
不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃甚至系统崩溃。
由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。
总结一下,使用ThreadLocal会发生内存泄漏的前提条件如下:
(1)线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。
(2)ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。
综合以上两点可以看出:使用ThreadLocal出现内存泄漏还是比较容易的。但是一般公司对如何使用ThreadLocal都有编程规范,只要大家按照规范编写程序,也没有那么容易发生内存泄漏。
2. ThreadLocal内存泄漏解决方案?
编程规范推荐使用static final修饰ThreadLocal对象。
ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内的所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。另外,为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更。
@Data
public class Person {
private int age = 10;
}
public class Test {
private static final ThreadLocal<Person> threadLocal = new ThreadLocal<>();
}
凡事都有两面性,使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象(person)一直存在强引用,于是Value指向的对象(person)在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。
总结:
由于ThreadLocal使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用ThreadLocal时遵守以下两个原则:
(1)尽量使用private static final修饰ThreadLocal实例。使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。
(2)ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。
总之,使用ThreadLocal能实现每个线程都有一份变量的本地值,其原因是每个线程都有自己独立的ThreadLocalMap空间,本质上属于以空间换时间的设计思路,该设计思路属于另一种意义的“无锁编程”。
|