ThreadLocal 内存泄漏和常见问题详解

ThreadLocal 知识储备传送门:

ThreadLocal 原理及源码详解

ThreadLocal 实战使用详解

什么是内存泄漏(Memory Leak)?

内存泄漏(Memory Leak):是指程序在申请内存后,未能正确释放,导致系统内存的浪费,可能会使程序运行速度减慢甚至系统崩溃。内存泄漏通常会逐渐积累,降低系统整体性能,极端情况下可能导致系统崩溃。

在分析ThreadLocal 内存泄漏之前,我们先看一段 ThreadLocalMap 的源码,如下:

static class Entry extends WeakReference<ThreadLocal<?>> {

	Object value;

	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}

从上述源码中我们看到 Entry 继承了 WeakReference<ThreadLocal<?>>,即 Entry 的 key 是弱引用,弱引用对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它。

如果 Entry 使用了强引用,也就是 key 使用了强引用,ThreadLocal 会发生内存泄漏吗?

答案是:会,业务使用完 ThreadLocal 后,因为 ThreadLocalMap 的 Entry 是强引用,也就是说 ThreadLocal 虽然使用完了,但是还是被 ThreadLocalMap 的 Entry 引用在,自然不会被垃圾回收,对应的 value 也不会被回收,会造成更严重 Entry 引起的内存泄漏。

为了方便理解,我画了一个 ThreadLocal 引用简图来方便理解,如下:

在这里插入图片描述
分析:

我们知道 TheadLocaL 变量对 ThreadLocal 对象有强引用,如果 TheadLocaLMap 的 Entry 的 key 也持有 ThreadLocal 对象的强引用,那即使 TheadLocaL 生命周期结束了,设置成 null 了,但由于 TheadLocaLMap 的 Entry 的 key 持有 ThreadLocal 的强引用,导致 ThreadLocal 不会被 JVM GC 回收,同时 value 就跟不会被 JVM GC 回收了,如果 TheadLocaLMap 的 Entry 的 key 持有 ThreadLocal 的是弱引用,此时当 ThreadLocal 被设置为 null 的时候,由于弱应用的特性,JVM GC 时候就会对 ThreadLocal 进行回收,最多就是 value 没有被回收。

强、弱、软、虚引用简单认识:

  • 强引用(Reference):垃圾回收器绝不会回收它,当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收有强引用的对象来解决内存不足问题。
  • 弱引用(WeakReference):弱引用对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它。
  • 软引用(SoftReference):如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,只要垃圾回收器没有回收它。
  • 虚引用(PhantomReference):它就和没有任何引用一样,在任何时候都可能被垃圾回收。

ThreadLocal 内存泄漏的真正原因是什么?

通过对比,我们知道了内存泄漏跟强、弱引用没有关系,其实内存泄漏主要跟一下两点有关系:

  • Entry 是否被删除:因为 Entry 持有了 ThreadLocal 的引用,那纸用完 ThreadLocal 后删除对应的 Entry,就能避免内存泄漏。
  • 当前 Thread 线程是否结束:因为 ThreadLocal 是 Thread 的一个属性,而 ThreadLocalMap 又是 ThreadLocal 的内部类,因此可以理解 ThreadLocalMap 是 Thread 的一个属性,ThreadLocalMap 的生命周期是伴随着 Thread 的生命周期,如果使用完 ThreadLocal 后 Thread 还一直运行,自然就会造成 ThreadLocal 的内存泄漏。

ThreadLocalMap 为什么要使用弱引用?

上面我们已经分析了,无论 ThreadLocalMap 的 key 使用强引用还是弱引用,都会可能存在内存泄漏的可能,使用弱引用会比强引用多一层保障,弱引用的 key 会被 JVM 回收掉,至少比强引用导致的 Entry 内存泄漏要强,当 key 是弱引用,在引用的 ThreadLocal 的对象被回收之后,ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,在系统内存不足的时候,利用弱引用的特性,key也会被 JVM GC 回收,这样 value 在下一次 ThreadLocalMap 调用 get,set,remove 等方法的时候也会被清除,也可以一定程度的避免内存泄漏。

避免内存泄漏的方法?

  • 使用完 ThreadLocal 后,手动删除对应的 Entry(ThreadLocal 提供了 remove 方法)。
  • 使用完 ThreadLocal 后,当前线程随之结束。

ThreadLocal 的设计是怎么避免内存泄漏的?

ThreadLocal 的实现 ThreadLocalMap 除了提供了 remove 方法来删除 Entry 之外,ThreadLocalMap 中的 set 方法中除了会设置值之外,还会找出 key 为 null 的 Entry,将其 value 设置为 null,这样下次 JVM GC 的时候就可以被回收了,有效的避免了内存泄漏。

为什么使用 ThreadLocal 时建议使用 static 修饰?

为了避免重复创建 TSO(thread specific object 即与线程相关的变量),导致内存浪费,正常来说一个 ThreadLocal 实例对应当前线程中的一个 TSO 实例,如果把 ThreadLocal 声明为某个类的实例变量,而不是静态变量,在同一个线程中每创建一个该类的实例就会导致一个新的 TSO 实例被创建,这就导致了一个一个线程对应多个 ISO 实例,而这多个 TSO 实例是同一个类的实例,且不说这会不会导致错误,这至少会导致内存浪费,因此才建议使用 static 修饰 ThreadLocal, 使用 static 修饰后,所有此类的实例共享这个静态变量,该静态变量只在类加载的时候装载,在当前线程内的此类对象都可以操作这个静态变量。

ThreadLocal 可以解决线程安全问题吗?

**请牢记 ThreadLocal 不能解决线程安全问题,**ThreadLocal 的诞生并不是为了解决线程安全问题的,而是设计了一种将变量帮绑定到当前线程的机制,更通俗的来说 ThreadLocal 就是为每个线程创建了自己的局部变量,也就是说每个线程中的局部变量在 JVM 中的引用地址没有指向同一个地址,本质就不是同一个变量,因此 ThreadLocal 跟线程安全完全是两回事,简单来说 ThreadLocal 的用处就是把局部变量共享成全局变量,方便变量在线程生命周期中的多个方法中使用。

为什么用 ThreadLocal 做key?

这个问题我们反着思考,如果使用 Thread 作为 key,一个线程中有多个 ThreadLocal 的时候我们该怎么办?因此,不能使用 Thread 做key,而要使用 ThreadLocal 对象做 key,这样可以通过 ThreadLocal 对象的 get 方法准确的获取到 ThreadLocal 对象。

TheadLocalMap 出现了 hash 碰撞后怎么处理的?

ThreadLocalMap 使用的是开放寻址来处理 hash 碰撞的,hash 的使用场景很多,哈希散列表是一种高效的数据结构,能将查找的时间复杂度降到O(1),使用通过 hash 函数来生成一个 hashcode 值,来完成对数据的定位,但是我们知道 hashcode 会重复,当发生 hash 冲突后,一般有三种处理方式,如下:

  • 拉链法:每个哈希表节点都有一个 next 指针,多个发生 hash 碰撞的节点会构成一个链表,用链表来存储这些数据,拉链法处理冲突简单,适合用在大量数据的场景,但是拉链法指针占用较大空间时,会造成空间浪费,我们熟知的 HashMap 就是采用的拉链法。
  • 开放定址法:当发生了哈希冲突,就去寻找下一个空的散列地址,只要散列表足够大,就一定可以找到下一个空的地址,把值存入进去,但是容易产生堆积问题,不适于大规模的数据存储。
  • 再哈希:其实就是多次 hash,使用多个 hash 函数,如果一次 hash 发生冲突,就接着进行 hash,直到无冲突为止。

为什么 ThreadLocalMap 使用开放寻址法?个人理解跟 ThreadLocalMap 本身存储的数据较少有关系,我们知道 ThreadLocalMap 存储的 ThreadLocal 变量,而 ThreadLocal 变量一般不会太多,这也就导致了发生 hash 冲突的概率较小,而节点较少的时候,使用开发寻址较为节省空间,而相反链式寻址法,一般用于在冲突较多的情况,且空间占用较大,就不太适合 ThreadLocalMap 这个场景了。

欢迎提出建议及对错误的地方指出纠正。

  • 50
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal内存模型是指通过ThreadLocal类来实现的一种特殊的线程封闭机制,它可以让每个线程都拥有自己的变量副本,从而实现线程间的数据隔离。 在使用ThreadLocal时,每个线程都有自己独立的ThreadLocalMap实例,该实例存储了线程自身的ThreadLocal变量。实际上,数据是保存在当前的Thread对象上,而不是ThreadLocal对象上。ThreadLocal只是提供了一个操作的框架,用于将数据存储在当前线程的ThreadLocalMap中。 ThreadLocal内存泄漏是指当ThreadLocal对象被回收时,由于ThreadLocalMap中的Entry仍然持有对ThreadLocal对象的强引用,导致ThreadLocal对象无法被垃圾回收,从而造成内存泄漏。要避免内存泄漏,需要在不再使用ThreadLocal对象时手动调用其remove方法来清除对应的Entry。 ThreadLocal内存模型可以通过以下代码示例来观察: ```java import java.util.ArrayList; 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("运行结束"); } } ``` 在上述代码中,每个工作线程创建了15个ThreadLocal实例,并将其添加到一个ArrayList中。在每个ThreadLocal实例中,我们设置了一个ValueObject对象作为值。如果没有手动调用remove方法来清除Entry,那么在垃圾回收时,这些Entry将持续引用ThreadLocal对象,导致内存泄漏。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [java(8)--线程ThreadLocal详解](https://blog.csdn.net/hguisu/article/details/8024799)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [ThreadLocal 内存模型、内存泄漏原因、现象观测、解决](https://blog.csdn.net/the_first_snow/article/details/105743395)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值