什么是内存泄漏?
不再用到的内存,没有及时释放,就叫做内存泄漏。
对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃。
ThreadLocal是怎么造成内存泄露的呢?
如果发生了下面的情况:
- 如果ThreadLocal是null了,也就是要被GC回收了,
- 但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。
总之,就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
我们细致的分析一下。
ThreadLocal 有两个引用链
ThreadLocalMap中的Key就是ThreadLocal对象,ThreadLocal 有两个引用链:
- 一个引用链是栈内存中ThreadLocal引用:
- 一个引用链是ThreadLocalMap中的Key对它的引用:
而对于Value(实际保存的值)来说,它的引用链只有一条,就是从Thread对象引用过来的,如下图:
上述过程分析后,就会出现如下的两种情况:
情况1: key的泄漏
情况2: value的泄漏
情况1:key的泄漏
栈上的ThreadLocal Ref引用不再使用了,即当前方法结束处理后,这个对象引用就不再使用了,
那么,ThreadLocal对象因为还有一条引用链存在,如果是强引用的话,这里就会导致ThreadLocal对象无法被回收,可能导致OOM。
情况1 的解决方案,使用弱引用解决 。
情况2: value的泄漏
情况2.假设我们使用了线程池,如果Thread对象一直被占用使用中(如在线程池中被重复使用
),但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。
这就意味着,Value这条引用链就一直存在,那么就会导致ThreadLocalMap无法被JVM回收,可能导致OOM,如上图。
情况2 ,比较严重。还得另想办法。
情况1的解决方案:使用弱引用,解决key的内存泄露
从如下ThreadLocal中内部类Entry代码可知:
Entry类的父类是弱引用WeakReference,ThreadLocal的引用k通过 WeakReference 构造方法传递给了 父类WeakReference的构造方法,
从而,ThreadLocalMap中的Key是ThreadLocal的弱引用,通过弱引用来解决内存泄露问题。
具体的代码如下
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key为弱引用
value = v;
}
}
}
栈内存中的ThreadLocal Ref引用不再使用了,即当当前方法结束处理后,这个key对象引用就不再使用了,
那么,如果这里 不用弱引用而是强引用的话,这里ThreadLocal对象因为还有一条引用链存在,所以就会导致他无法被回收,可能导致OOM。
回顾Java中4种引用类型
强引用(Strong Reference):
- 这是最常见的引用类型。一个对象具有强引用,垃圾收集器就不会回收它,即使系统内存空间不足。
- 示例:
Object obj = new Object();
在这里,obj
就是new Object()
的一个强引用。软引用(Soft Reference):
- 用来描述一些可能还有用但并非必需的对象。在系统将要发生内存溢出异常前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 在Java中,软引用是用来实现内存敏感的高速缓存。
- 示例:使用
java.lang.ref.SoftReference
类可以创建软引用。弱引用(Weak Reference):
- 这里讨论ThreadLocalMap中Entry类的重点。
- 弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
- 在Java中,弱引用是用来描述那些非关键的数据,在Java里用
java.lang.ref.WeakReference
类来表示。- 示例:使用
java.lang.ref.WeakReference
类可以创建弱引用。虚引用(Phantom Reference):
- 一个虚引用关联着的对象,在任何时候都可能被垃圾收集器回收,它不能单独用来获取被引用的对象。虚引用必须和引用队列(
ReferenceQueue
)联合使用。主要用来跟踪对象被垃圾回收的活动。- 虚引用对于一般的应用程序来说意义不大,主要使用在能比较精确控制Java垃圾收集器的高级场景中。
- 示例:使用
java.lang.ref.PhantomReference
类可以创建虚引用。
弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
至此,key的泄漏问题,JDK已经帮我们顺利解决。
更复杂的是: 如何解决value内存泄露问题?
情况2的解决方案:清理策略解决value内存泄露
为了解决value内存泄露问题,Java 的 ThreadLocal 实现了两大清理方式:
- 探测式清理(Proactive Cleanup)
- 启发式清理(Heuristic Cleanup) 。
源码:value的 探测式清理 :
当线程调用 ThreadLocal
的 get()
、set()
或 remove()
方法时,会探测式的去触发对 ThreadLocalMap 的清理。
此时,ThreadLocalMap 会检查所有键(ThreadLocal 实例),并移除那些已经被垃圾回收的key键及其对应的value 值。
这种清理是主动的,因为它是在每次操作 ThreadLocal 时进行的。
探测式清理(Proactive Cleanup)如何实现的呢?:
从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。
注意:这里把清理的开销放到了get、set操作上,如果get的时候无用Entry(Entry的Key为null)特别多,那这次get相对而言就比较慢了。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 将k=null的entry置为null
e.value = null;
tab[i] = null;
size--;
} else {
// k不为null,则rehash从新分配配置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
// 重新分配后的位置上有元素则往后顺延。
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
源码:value的启发式清理:
在 ThreadLocalMap
的 set() 方法中,有一个阈值(默认为 ThreadLocalMap.Entry 数组长度的 1/4)。
当 ThreadLocalMap 中的 Entry 对象被删除(通过键的弱引用被垃圾回收)并且剩余的 Entry 数量大于这个阈值时,会触发一次启发式清理操作。
这种清理是启发式的,因为它不是每次操作都进行,而是基于一定的条件和概率。
启发式清理(Heuristic Cleanup)如何实现?:
从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 移除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
业务主动清理:手动清除解决内存泄露
要知道,ThreadLocal的一个常见问题是内存泄露。
这通常发生在使用线程池的场景中,因为线程池中的线程通常是长期存在的,它们的ThreadLocal变量也不会自动清理,这可能导致内存泄漏。
前面讲了,JDK已经用尽全力去解决了,JDK 用了三个办法,来解决内存泄漏。
尽管有弱引用以及这些清理机制,但最佳实践业务主动清理,
业务上解决这个问题的一个方法是,每当使用完ThreadLocal变量后,显式地调用remove()
方法来清除它:
如何业务主动清理?在使用完 ThreadLocal 后显式调用 remove()方法
,以确保不再需要的值能够被及时回收,key和value 都同时清理,一锅端。
这样可以避免潜在的内存泄漏问题,并减少垃圾回收的压力。
讲到这里,尼恩团队给大家,用一个大的图总结一下 ThreadLocal的内存泄露与解决方案,具体如下: