Skip to main content

ThreadLocal什么场景内存泄露?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

当一个线程访问 ThreadLocal 的时候,实际上是访问线程对象持有的 ThreadLocalMap,每个 ThreadLocal 和它对应的数据都会被封装为 Entry 对象并存储到 ThreadLocalMap 中。

其中,Entry 对象将对 Key —— 也就是 ThreadLocal —— 的引用设置为了弱引用,由于弱引用的对象会在 GC 后被回收,因此在 GC 后 Entry 的 Key 将会变为 null,而线程对象本身对 Entry 又构成间接的强引用,因此若线程对象无法被回收,那么 Entry 与其占用的内存空间将始终得不到释放,此时就发生了内存泄露。

所以,当我们使用 ThreadLocal 后不主动调用 remove 方法时,是否发生内存泄露的关键就在于线程是否会被回收。

一般情况下,如果我们直接通过创建线程对象的方式创建线程,等到任务执行完了线程对象自然就会被回收。而当我们使用线程池时,若未设置允许线程池的核心线程超时,且恰好线程池中的工作线程池小于等于核心线程数,此时线程将不会因为超时而回收。并且,倘若该线程在执行过程中始终不会因为被标记为中断或抛出异常而导致被回收,那么这个线程将会一直存活下去。此时,该线程池持有的 ThreadLocalMap 中的数据也将不会被回收,此时就会发生内存泄露。

当然,ThreadLocalMap 本身在进行增删改查的时候会自动对失效的数据进行一定程度的清理,不过这里的清理始终是有限且滞后的,它只能视 JVM 分配的内存大小、访问 ThreadLocal 的线程数量、线程对 ThreadLocal 的访问频率……等因素,一定程度上减少因为内存泄露而导致发生 OOM 的可能性。

问题详解

1. 内存泄露与内存溢出

首先,我们需要明确一下内存泄露与内存溢出的区别:

  • 内存泄露(Memory Leak):程序申请了一块内存,后面出现了点问题,导致这一块内存既无法被访问,又无法被释放。
  • 内存溢出(Out Of Memory):因为某些原因,导致程序申请了超过配额的内存,最终导致 OOM。

当发生内存泄露时,程序会一直占用额外的内存而不释放,当占用的内存超过的操作系统允许其占用的最大内存后,就会出现内存溢出问题。

2. 可达性分析算法

在 Java 中,不同的垃圾回收器有着不同的垃圾回收算法,不过它们基本都基于可达性分析算法判断某个对象是否可以被回收。

简单的来说,当我们在某个对象中引用了其他的对象之后,它们将会形成引用关系,多个对象之间互相引用,最后会形成一个引用链或者图。

image.png

在这些对象中,有一部分是特殊的对象被称为 GCRoot,当要对一些对象进行垃圾回收时,将会从这些 GCRoot 开始遍历,确认通过引用链是否可以找到这些对象,如果找得到说明这些对象仍然还在被使用,不能回收,否则说明这些对象已经可以被回收。

3. 引用对象与内存泄露

在正常情况下,可达性分析算法可以保证我们没有使用的对象一定会被正常回收,而正在被使用的对象不会被回收,因此按照常理是不会出现内存泄露问题的。

但是,在 Java 中,又将引用关系分为了四种:

  • 强引用:即正常代码中,对象与对象的引用关系,只要一个对象被强引用,那么在引用消失前它都不会被 GC。
  • 软引用:通过 SoftReference 包装类间接的持有的对象,当 JVM 内存不足时就会自动回收。
  • 弱引用:通过 WeakReference包装类间接的持有的对象,当 JVM 发生 GC 时就会自动回收。
  • 虚引用:通过 PhantomReference包装类间接的持有的对象,它在任何时候都有可能被 JVM 回收。

其中,针对后三种引用对象的使用方式是这样的:

Object key = new Object(); // 被引用对象
WeakReference wr = new WeakReference(key); // 引用包装类
Entry<Object> entry = new Entry(wr, "value"); // 引用对象

解锁付费内容,👉 戳