虚引用(WeakReference)
在开始之前,需要区分 Java 引用中的强、软、弱、虚引用,ThreadLocal 使用了弱引用,它是问题的关键。
只有弱引用指向的对象,只要进行 GC 时便会被清除。
ThreadLocal 内存模型
每个线程自己保存一个 Map,即 ThreadLocalMap,这个 Map 以 ThreadLocal 对象为键,且 ThreadLocalMap 的 Entry 继承了 WeakReference 类,是一个对 ThreadLocal 对象的弱引用,源码如下(ThreadLocal.ThreadLocalMap.Entry)。
使用 ThreadLocal 时容易产生一种感觉,即我们保存的数据似乎保存在 ThreadLocal 对象上,实际上它们保存在当前的 Thread 对象上。ThreadLocal 只是提供了一个操作的框架。
ThreadLocal 内存泄漏原理
如果我们在线程中使用 ThreadLocal 不当,将会导致保存的数据无法被回收,过程如下:
ThreadLocal 内存泄漏现象观测
清楚了原理以及解决办法,我们通过一个程序来验证上述过程,并观测内存泄漏现象。
运行环境
OpenJDK13、G1 回收器、堆空间 20M
命令行参数:-Xmx20M -Xlog:gc*
测试代码
import java.util.ArrayList;
/** ThreadLocal 内存泄漏观测 */
public class Main {
static class ValueObject {
private long[] data = new long[131072]; // 需要 1M 空间 (1024 * 1024 / 8)
}
public static void main(String[] args) throws InterruptedException {
int threadNumber = 10;
while (threadNumber-- > 0) {
Thread worker = new Thread(() -> {
int localCount = 15;
var locals = new ArrayList<>(localCount);
while (localCount-- > 0) {
ThreadLocal<ValueObject> newLocal = new ThreadLocal<>();
newLocal.set(new ValueObject());
locals.add(newLocal);
// newLocal.remove();
}
locals = null;
System.gc();
}, "工作线程");
worker.start();
worker.join();
}
System.out.println("运行结束");
}
}
现象
输出的日志中,可以看到程序报 OutOfMemoryError,且即使垃圾回收器 Full GC 也无法释放内存。而将上文代码中 newLocal.remove 一行注释解开,则不会发生该错误,对比如下,可以发现二者对堆空间的使用上存在很大差距:
总结
解决这个问题,需要在用完 ThreadLocal 后调用 remove()。
另外通过实验发现,这个现象不是必然发生,分析 GC 日志可以发现,很多时候,垃圾回收器仍然可以成功回收一很大部分空间。