ThreadLocal 内存模型、内存泄漏原因、现象观测、解决

虚引用(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 日志可以发现,很多时候,垃圾回收器仍然可以成功回收一很大部分空间。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章