ThreadLocal 内存泄露分析
内存泄漏(memory leak ):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免
线程Thread对象中,每个线程对象内部都有一个的ThreadLocalMap对象。如果这个对象存储了多个大对象,则可能早出内存溢出OOM。为了防止这种情况发生,在ThreadLocal的源码中,有对应的策略,即调用 get()、set()、remove() 方法,均会清除 ThreadLocal内部的 内存。
ThreadLocal的内部是ThreadLocalMap。ThreadLocalMap内部是由一个Entry数组组成。Entry类的构造函数为 Entry(弱引用的ThreadLocal对象, Object value对象)。因为Entry的key是一个弱引用的ThreadLocal对象,所以在 垃圾回收 之前,将会清除此Entry对象的key。那么, ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。若在指定的线程任务里面,调用ThreadLocal对象的get()、set()、remove()方法,可以避免出现内存泄露。
下图虚线表示弱引用。ThreadLocal对象被GC回收了,那么key变成了null。Map又是通过key拿到的value的对象。所以,GC在回收了key所占内存后,没法访问到value的值,因为需要通过key才能访问到value对象。如图所示的引用链:CurrentThread → Map(ThreadLocalMap) → Entry → value ,所以在当线程没有被回收的情况下,value所占内存也不会被回收。所以可能会造成了内存溢出。
虚线表示是弱引用。弱引用只要继承WeakReference<T>类即可。所以说,当ThreadLocal对象被GC回收了以后,Entry对象的key就变成null了。这个时候没法访问到 Object Value了。并且最致命的是,Entry持有Object value。所以,value的内存将不会被释放。
因为上述的原因,在ThreadLocal这个类的get()、set()、remove()方法,均有实现回收 key 为 null 的 Entry 的 value所占的内存。所以,为了防止内存泄露(没法访问到的内存),在不会再用ThreadLocal的线程任务末尾,调用一次 上述三个方法的其中一个即可。
因此,可以理解到为什么JDK源码中要把Entry对象,用 弱引用的ThreadLocal对象,设计为key,那是因为要手动编写代码释放ThreadLocalMap中 key为null的Entry对象。
GC什么时候回收弱引用的对象?弱引用对象是存活到下一次垃圾回收发生之前对象。
综上:JVM就会自动回收某些对象将其置为null,从而避免OutOfMemory的错误。弱引用的对象可以被JVM设置为null。我们的代码通过判断key是否为null,从而 手动释放 内存泄露的内存。
为什么要将ThreadLocal设计为弱引用?
因为弱引用的对象的生命周期直到下一次垃圾回收之前被回收。弱引用的对象将会被置为null。我们可以通过判断弱引用对象是否已经为null,来进行相关的操作。在ThreadLocalMap中,如果键ThreadLocal已经被回收,说明ThreadLocal对象已经为null,所以其对应的值已经无法被访问到。这个时候,需要及时手动编写代码清理掉这个键值对,防止内存泄露导致的内存溢出。
1 强引用
强引用虽然在开发过程中并不怎么提及,但是无处不在,例如我们在一个对象中通过如下代码实例化一个StringBuffer对象
StringBuffer buffer = new StringBuffer();
我们知道StringBuffer的实例通常是被创建在堆中的,而当前对象持有该StringBuffer对象的引用,以便后续的访问,这个引用,就是一个强引用。
对GC知识比较熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般默认是可达性算法,即在每一轮GC的时候,选定一些对象作为GC ROOT,然后以它们为根发散遍历,遍历完成之后,如果一个对象不被任何GC ROOT引用,那么它就是不可达对象,则在接下来的GC过程中很可能会被回收。
强引用最重要的就是它能够让引用变得强(Strong),这就决定了它和垃圾回收器的交互。具体来说,如果一个对象通过一串强引用链接可到达(Strongly reachable),它是不会被回收的。如果你不想让你正在使用的对象被回收,这就正是你所需要的。
2 软引用
软引用是用来描述一些还有用但是并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收返回之后进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后提供了SoftReference来实现软引用。
相对于强引用,软引用在内存充足时可能不会被回收,在内存不够时会被回收。
3 弱引用
弱引用也是用来描述非必须的对象的,但它的强度更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。
4 虚引用
虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个瑞祥是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被GC时收到一个系统通知。JDK1.2之后提供了PhantomReference来实现虚引用