强与弱引用与 GC 的具体交互(ThreadLocal)

这是理解 ThreadLocal 内存泄漏根源的最后一公里。

从 JVM 垃圾回收的底层视角,彻底搞清楚强、软、弱、虚四种引用类型与 GC 的交互机制。


1. 四种引用类型的强度层级

从强到弱依次为:

强引用(Strong Reference) > 软引用(Soft Reference) > 弱引用(Weak Reference) > 虚引用(Phantom Reference)

这个"强度"指的是对象被 GC 回收的难易程度


2. 各引用类型与 GC 的具体交互

2.1 强引用(Strong Reference)

这是我们 99% 场景使用的引用方式:

java 复制代码
Object obj = new Object();  // obj 就是强引用

GC 行为:

  • 只要强引用链存在,对象永远不会被回收,即使发生 OOM。

  • 可达性分析中,从 GC Roots 能追踪到的对象都是强可达(Strongly Reachable)。

回收时机: 只有当强引用被显式置为 null,或者引用变量超出作用域,对象失去强引用后,才会在下次 GC 时被回收。


2.2 软引用(Soft Reference)

用于实现内存敏感缓存:

java 复制代码
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get();  // 可能返回 null

GC 行为:

  • 内存充足时 ,软引用对象不会被回收

  • 发生 OOM 之前 (即内存即将耗尽时),GC 会尽力回收所有软可达(Softly Reachable)对象。

具体实现(以 HotSpot 为例):

JVM 会根据当前堆内存使用情况和 -XX:SoftRefLRUPolicyMSPerMB 参数(默认 1000ms)决定回收策略。这意味着软引用对象在内存紧张时会被批量清除。

典型场景: 图片缓存、大对象缓存。


2.3 弱引用(Weak Reference)⭐

这是 ThreadLocal 使用的引用类型:

java 复制代码
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get();  // 可能返回 null

GC 行为:

  • 只要发生 GC,无论内存是否充足,弱引用对象都会被回收

  • 在下一次 GC 运行时,弱可达(Weakly Reachable)的对象会被立即标记清除。

关键时序:

重要细节:

  • 弱引用对象被回收并不意味着弱引用对象本身被回收,只是它指向的目标对象被回收了。

  • 回收后,弱引用会被 JVM 自动注册到关联的 ReferenceQueue(如果创建时指定了)。

典型场景: ThreadLocal、WeakHashMap。


2.4 虚引用(Phantom Reference)

最弱的引用,几乎无法通过它获取对象:

java 复制代码
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
Object obj = phantomRef.get();  // 永远返回 null

GC 行为:

  • 虚引用对象任何时候都可能被回收

  • 主要用途是对象回收跟踪 ,通过 ReferenceQueue 感知对象何时被回收。

典型场景: 直接内存(DirectBuffer)的清理,NIO 中用于监控堆外内存回收。


3. GC Roots 可达性分析可视化

引用强度决定了对象在 GC 中的"生存优先级":

  • 红色(强引用):只要 GC Root 可达,绝对不回收。

  • 黄色(软引用):内存不足时才回收。

  • 绿色(弱引用):GC 发生时立即回收。


4. ThreadLocal 中的弱引用交互细节

4.1 Entry 的弱引用机制

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;  // 强引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // key 作为弱引用
        value = v;
    }
}

场景模拟:

java 复制代码
// 1. 创建 ThreadLocal 对象,强引用 tl 指向它
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("hello");  // 当前线程的 Map 中,Entry 的 key 弱引用指向 tl

// 2. 将强引用 tl 置为 null
tl = null;

// 3. 触发 GC
System.gc();

// 发生了什么:
// - tl 不再指向 ThreadLocal 对象,该对象只剩下 Entry 中的弱引用
// - GC 发现只有弱引用,立即回收 ThreadLocal 对象
// - Entry 中的 key 变为 null,但 value 仍然强引用着 "hello" 字符串

4.2 为什么 Entry 中的 value 是强引用?

这是问题的根源!如果 value 也是弱引用,数据随时可能丢失,那 ThreadLocal 就没有实用价值了。所以:

  • Key(ThreadLocal):弱引用,允许自身被回收。

  • Value(用户数据):强引用,必须保证数据有效。


5. 内存泄漏的完整链条

泄漏条件:

  1. ThreadLocal 对象失去外部强引用 → 被 GC 回收

  2. Entry.key 变为 null

  3. 但 Entry.value 仍然被 ThreadMapEntry 这条强引用链持有

  4. 如果当前线程是核心线程池线程(永不销毁),这条引用链永久存在

  5. Value 对象永远无法被回收 → 内存泄漏


6. ReferenceQueue 的作用

ThreadLocalMap 在创建 Entry 时没有显式传入 ReferenceQueue ,因为 JDK 设计者采用了主动清理策略而非依赖队列通知。

WeakHashMap 等类会使用 ReferenceQueue:

java 复制代码
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> ref = new WeakReference<>(new Object(), queue);
// GC 后,ref 会被放入 queue
// 应用可以轮询 queue,感知对象被回收了

ThreadLocal 选择主动清理而非队列,是因为:

  • 队列通知是异步、被动的,需要额外线程处理。

  • 主动清理在 get/set/remove 时顺带进行,更简单高效。


7. 实验验证

java 复制代码
public class ReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        WeakReference<Object> weakRef = new WeakReference<>(new Object());
        System.out.println("GC前: " + weakRef.get());  // 输出对象
        
        System.gc();
        Thread.sleep(100);
        System.out.println("GC后: " + weakRef.get());  // 输出 null
        
        // ThreadLocal 实战
        ThreadLocal<String> tl = new ThreadLocal<>();
        tl.set("important data");
        System.out.println("GC前: " + tl.get());  // important data
        
        tl = null;  // 断开强引用
        System.gc();
        Thread.sleep(100);
        // 此时 ThreadLocal 对象已被回收,但 value 还在内存中(无法访问)
        // 只能通过反射看到 Thread.currentThread().threadLocals 中还有遗留数据
    }
}

8. 总结:GC 交互的关键规律

引用类型 GC 行为 回收时机 ThreadLocal 中使用
强引用 绝对不回收 永远(除非手动置 null) Entry.value
软引用 内存紧张时回收 OOM 前 不适用
弱引用 每次 GC 都回收 下次 GC 运行时 Entry.key
虚引用 随时回收,无法获取对象 任何时候 不适用

最核心的一句记忆口诀:

弱引用是"见光死"------每次 GC 必回收;强引用是"钉子户"------只要引用链在,雷打不动。