ThreadLocal 内存泄漏
编辑ThreadLocal 是一个实现线程本地存储数据的工具,每个线程保存独立的数据副本,避免线程之间发生数据共享。ThreadLocal 的内存泄漏原因点主要在于 ThreadLocalMap 的 key 是弱引用,以及 GC 对弱引用的回收机制。
工作原理
了解 ThreadLocal 的工作原理,先看看关键源码
public class ThreadLocal<T> {
/**
* 取值,从当前线程的属性 threadLocals 中取值
*/
public T get() {
ThreadLocalMap map = Thread.currentThread().threadLocals;
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* 写值,写入当前线程的属性 threadLocals 中
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null)
map.set(this, value);
else
t.threadLocals = new ThreadLocalMap(this, value)
}
/**
* 线程存储本地数据的Map结构, 键为 ThreadLocal 对象本身, 且标记为弱引用, 值为需要存储的数据
*/
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
ThreadLocalMap 是存储数据的结构,在 ThreadLocal 中定义,但实际创建对象是作为 Thread 的一个属性
public class Thread implements Runnable {
/**
* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
它们的关系是
Thread -> ThreadLocalMap -> (ThreadLocal, 存储数据)
画个图辅助理解
GC 对 弱引用 的回收机制
在发生 GC 时,只要发现被弱引用修饰的对象没有其他任何强引用指向,就会将该对象回收,不论内存空间是否有空闲。
内存泄漏原因
这意味着,没有强引用指向 ThreadLocal 对象时,ThreadLocal 对象就会被回收,在 ThreadLocalMap 中体现为
Thread -> ThreadLocalMap -> (null, 存储数据)
这样,我们无法再访问到 存储数据
这一块对象,又因为存在 ThreadLocalMap.Entry 的引用指向,导致无法被 GC 回收,即发生内存泄漏,泄漏的内存为 存储数据
占用的内存空间,即 ThreadLocalMap 中的 Value。
直到该线程生命终止,ThreadLocalMap 一整个被回收掉。
public class Example {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
local.set("存储数据");
// 清除强引用
local = null;
// 若此时发生 GC,ThreadLocal 实例会被回收
// System.gc();
}
}
内存泄漏场景
在一般项目中,我们不会持续创建线程销毁线程,因为系统资源开销高,通常使用线程池。比如,Tomcat 连接池、一般业务线程池等等,线程池中的线程不会因为任务完成而终止,这意味着线程会在整个应用的生命周期内持续运行。
假设线程池维持 200 个线程,每个线程维护一个 ThreadLocalMap,若应用中使用 10 个 ThreadLocal 对象,ThreadLocal 存储数据大小 1 MB,则有可能造成
200 * 10 * 1 MB = 2000 MB
大小的内存泄漏。
安全使用
一般的解决方案就是在一次使用完成后及时清除数据,避免残留在 ThreadLocalMap 中
public class Example {
private ThreadLocal<String> local = new ThreadLocal<>();
public void example() {
try {
local.set("存储数据");
// do others...
} finally {
local.remove();
}
}
}
使用场景
在 SpringBoot 构建的 Web 项目中,通常可以用来传递当前请求的上下文信息、用户Session、数据源切换等场景,一般是配合过滤器、AOP等技术实现自动设置、清除数据,以保证不会发生内存泄漏的情况。
为什么要把 ThreadLocalMap 的 key 设计成弱引用?
本质原因还是为了解决 ThreadLocal 对象本身内存泄漏的问题。
如果 key 设置为强引用,那么,当外部清除 ThreadLocal 对象的强引用时,此时 key 依然保持对 ThreadLocal 对象的强引用,导致 ThreadLocal 对象本身无法被回收。
public class Example {
private ThreadLocal<String> local = new ThreadLocal<>();
public void example() {
local.set("存储数据");
// 清除强引用
local = null;
// 如果 key 设置为强引用,当前线程的 ThreadLocalMap 还有强引用指向 ThreadLocal 对象(local)
// 此时如果发生 GC 无法回收 local 对象占用的空间
// System.gc();
}
}
- 0
- 0
-
分享