Java 四种引用类型与 ThreadLocal 内存泄漏

java-concurrency 📚 learning jvm-reference-types · jvm · reference · soft · weak · phantom · threadlocal · leak

TL;DR(30 秒扫完)

  • 四种引用:强(默认)/ 软(内存敏感缓存)/ 弱(下次 GC 回收)/ 虚(对象死亡钩子)
  • 回收优先级:虚 < 弱 < 软 < 强
  • 典型场景:软→LRU 缓存;弱→ThreadLocal Key、WeakHashMap;虚→DirectByteBuffer Cleaner
  • ThreadLocal 内存泄漏陷阱:Key 用弱引用可回收,但 Value 仍强引用,需手动 remove()
  • PhantomReference.get() 永远返回 null——它不是"引用对象",而是"监听对象死亡"

关键结论

结论 A四种引用强度对应四种回收时机(GC 触发 / OOM 前 / 永不自动 / 只通知)
结论 B软引用可用 new SoftReference<>(obj, maxCapacity) 精细控制
结论 CThreadLocal 的 Value 是强引用,弱引用只解决 Key 泄漏,Value 仍需手动 remove()
结论 D生产环境缓存建议用 Caffeine/Guava Cache,底层封装了软/弱引用 + ReferenceQueue

完整讲解(费曼四步)

STEP 1 · 概念

Java 引用强度分四级:强引用 > 软引用 > 弱引用 > 虚引用。强度越低,回收越激进。每种引用对应一种使用场景:默认对象、内存敏感缓存、辅助结构、对象死亡钩子。

STEP 2 · 大白话

类比:把对象比作"员工",把 GC 比作"HR"。强引用像"编制内员工",HR 不主动裁;软引用像"外包员工",公司预算紧张就先裁;弱引用像"临时工",只要 HR 来巡视就裁;虚引用像"离职顾问",其实没工作,但会通知 HR 他离职了。

STEP 3 · 底层

四种引用对照

引用类回收时机典型场景关键点
强引用Object o = new Object()永不自动回收常规对象内存不足抛 OOM
软引用SoftReference内存不足时回收,可配 maxCapacity图片缓存、Session 缓存new SoftReference<>(obj, maxCapacity)
弱引用WeakReference下次 GC 就回收ThreadLocal Key、WeakHashMap单独用会被回收,需封装
虚引用PhantomReferenceget() 永远 nullDirectByteBuffer 清理、对象死亡钩子必须搭 ReferenceQueue
回收优先级:虚 < 弱 < 软 < 强

ThreadLocal 内存泄漏的真相

ThreadLocal 内部 ThreadLocalMap 的结构:
// 简化后的 ThreadLocalMap 结构
Entry extends WeakReference<ThreadLocal<?>> {
  Object value;   // Value 是强引用!
}
  • Key 是 WeakReference:线程不死时让 Key 可回收(避免 ThreadLocal 对象本身泄漏)
  • Value 是强引用:Key 被回收后,Value 依然占用内存 → "幽灵键"泄漏
  • 解决:手动 try-finally 包裹 + ThreadLocal.remove()
threadLocal.set(value);
try {
  // 使用
} finally {
  threadLocal.remove();   // 关键!
}

PhantomReference 的正确用法

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);

// phantom.get() 永远返回 null
// 对象被 GC 时,phantom 会被加入 queue
// 消费 queue 得到"对象死亡通知"

// 典型场景:DirectByteBuffer Cleaner 内部实现

WeakHashMap 使用陷阱

  • WeakHashMap 的 Key 用弱引用,Value 是强引用
  • 单独使用仍可能泄漏(Value 一直持有)
  • 依赖 GC 触发时机,不能保证立即释放

STEP 4 · 简化

一句话总结:四种引用 = 四种回收时机。虚<弱<软<强。ThreadLocal 用弱引用做 Key 但 Value 强引用,必须手动 remove()。生产环境缓存用 Caffeine/Guava。
记忆口诀:
  • 强:常规对象,永不 GC
  • 软:内存紧张才 GC(软引用 = 内存缓存)
  • 弱:下次 GC 就 GC(弱引用 = ThreadLocal Key)
  • 虚:只收通知,get 永远 null(虚引用 = DirectByteBuffer 清理)

常见误区

"弱引用会优先回收"
弱引用下次 GC 就回收,时机不由开发者决定;"优先"是错觉
"ThreadLocal 用弱引用就不会泄漏"
Key 弱引用可回收,Value 仍强引用,需手动 remove()
"PhantomReference 是用来引用对象的"
get() 永远返回 null,本质是"对象死亡钩子"
"虚引用和弱引用差不多"
虚引用只通知不持有,弱引用可 get() 拿到对象
"软引用 = 强引用"
软引用受 -XX:SoftRefLRUPolicyMSPerMB 影响存活时长,可配 maxCapacity

延伸追问

ThreadLocal 为什么用弱引用?为什么还会泄漏?
Key 用 WeakReference 让 Key 可回收;但 Value 仍强引用,若线程池线程不死,Key 被回收后变成"幽灵键",Value 依然占用内存;解决靠手动 remove()
软引用如何做精细的 LRU 缓存?
设置 SoftReference 的 maxCapacity,配合 ReferenceQueue 主动清理已回收的引用;生产更推荐 Caffeine/Guava Cache
DirectByteBuffer 泄漏如何排查?
JDK 8 用 PhantomReference + ReferenceQueue;JDK 9+ 用 Cleaner;监控 jcmd VM.native_memory、JFR jdk.DirectByteBuffer、-XX:MaxDirectMemorySize
WeakHashMap 会避免泄漏吗?
单独看不能(值仍强引用),配合 GC 触发时机;不能保证立即释放
Caffeine 内部用了哪些引用策略?
weakKeys / weakValues / softValues / expireAfterAccess / expireAfterWrite 组合;配合 ReferenceQueue 主动清理

速查表

四种引用: 强(默认)> 软(缓存)> 弱(下次GC)> 虚(死亡钩子)
回收优先级: 虚 < 弱 < 软 < 强
典型场景: 软→LRU / 弱→ThreadLocal Key / 虚→DirectByteBuffer
ThreadLocal 泄漏: Key 弱引用 / Value 强引用 / 手动 remove()
生产缓存: Caffeine 或 Guava Cache(不必裸用引用类型)

Anki 候选卡片

Q: 四种引用回收优先级?
A: 虚 < 弱 < 软 < 强
Q: PhantomReference.get() 返回什么?
A: 永远 null,只能搭配 ReferenceQueue 使用
Q: ThreadLocal 内部 Key 用哪种引用?
A: WeakReference(Key 可回收),但 Value 仍强引用
Q: ThreadLocal 内存泄漏怎么解?
A: 手动 remove(),或 try-finally 包裹
Q: SoftReference 怎么设容量?
A: new SoftReference<>(obj, maxCapacity)

关联题目

  • ⚠️ 《什么是强引用、软引用、弱引用和虚引用?》— Round 1 Q2, ⭐⭐(软引用完全没答,虚引用只"听说")
  • 关联知识

    四种引用强度对应四种回收时机:强(默认)> 软(OOM 前)> 弱(下次 GC)> 虚(对象死亡钩子)
    对象像员工,GC 像 HR:强=编制内、软=外包(预算紧先裁)、弱=临时工(巡视就裁)、虚=离职顾问(没工作但会通知)
    ✦ 记 忆 口 诀 ✦
    回收优先级:虚<弱<软<强;ThreadLocal Value 是强引用,弱引用只解决 Key 泄漏
    关键可视化
    四种引用对照
    flowchart TB
      R["引用强度"] --> S["强引用<br/>Object o = new Object()<br/>永不自动回收<br/>常规对象<br/>OOM 时抛 OutOfMemory"]
      R --> Soft["软引用<br/>SoftReference<T><br/>内存不足时回收<br/>图片缓存、Session 缓存<br/>可配 maxCapacity"]
      R --> Weak["弱引用<br/>WeakReference<T><br/>下次 GC 就回收<br/>ThreadLocal Key、WeakHashMap<br/>单独用会被回收"]
      R --> Phantom["虚引用<br/>PhantomReference<T><br/>get() 永远 null<br/>DirectByteBuffer 清理<br/>对象死亡钩子"]
    ThreadLocal 内存泄漏的真相
    flowchart LR
      T["ThreadLocalMap"] --> E["Entry extends WeakReference<ThreadLocal<?>>"]
      E --> K["Key = WeakReference<br/>线程不死时让 Key 可回收<br/>避免 ThreadLocal 对象本身泄漏"]
      E --> V["Value = 强引用<br/>Key 被回收后 Value 依然占内存<br/>幽灵键泄漏"]
      V -.解决.-> F["try-finally 包裹<br/>ThreadLocal.remove()"]
    ReferenceQueue 通知机制
    flowchart LR
      R["引用对象"] --> Q["ReferenceQueue"]
      Q --> P["PhantomReference 入队<br/>通知对象死亡"]
      Q --> C["Cleanup 线程处理<br/>释放堆外内存"]
      P -.-> D["DirectByteBuffer 释放<br/>Cleaner 实现"]
    知识关系

    ⬆️ 前置(Prerequisite)

    jvm-memory-model

    🔄 延伸(Extends)

    暂无

    ⚡ 对比(Contrast)

    GC 算法— GC 算法决定'怎么回收',引用类型决定'回收时机'——两者配合工作
    🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面