【JVM】四大引用类型分析
- 【一】引用分类
-
- [【1】强引用 Strong Reference(默认引用)](#【1】强引用 Strong Reference(默认引用))
-
- (1)定义与作用
- (2)代码案例
- (3)强引用的场景
-
- 1-普通局部变量引用(方法内)
- 2-成员变量(实例变量)
- [3-静态变量 /static 静态集合(最容易内存泄漏)](#3-静态变量 /static 静态集合(最容易内存泄漏))
- 4-数组中存储对象
- [5-容器集合(ArrayList/HashMap/HashSet 等)](#5-容器集合(ArrayList/HashMap/HashSet 等))
- [6-ThreadLocal 存储的值(极易内存泄漏)](#6-ThreadLocal 存储的值(极易内存泄漏))
- 7-方法参数、返回值传递
- [8-内部类 / 匿名内部类 / Lambda 持有外部对象](#8-内部类 / 匿名内部类 / Lambda 持有外部对象)
- [9-循环引用(A 持有 B,B 持有 A)](#9-循环引用(A 持有 B,B 持有 A))
- 10-本地变量缓存、临时引用赋值
- [11-JNI / 本地方法持有 Java 对象](#11-JNI / 本地方法持有 Java 对象)
- 12-常量池、字符串常量引用
- (4)快速总结:哪些属于强引用
- (5)开发注意事项
- [【2】软引用 SoftReference(缓存专用)](#【2】软引用 SoftReference(缓存专用))
- [【3】弱引用 WeakReference(临时关联、无强制存活)](#【3】弱引用 WeakReference(临时关联、无强制存活))
- [【4】虚引用 PhantomReference(最弱,仅用于堆外内存回收)](#【4】虚引用 PhantomReference(最弱,仅用于堆外内存回收))
- 【二】四种引用对比总表
- 【三】通用开发规范与避坑总结
-
- 【1】内存缓存选型规范
- 【2】通用内存泄漏风险点
- [【3】GC 与引用开发最佳实践](#【3】GC 与引用开发最佳实践)
- 【4】问题延伸
- 【四】ThreadLocal的引用分析案例
-
- 【1】底层存储结构前置认知
-
- [(1)Thread 线程对象](#(1)Thread 线程对象)
- [(2)ThreadLocalMap(自定义哈希表,非 HashMap)](#(2)ThreadLocalMap(自定义哈希表,非 HashMap))
- [(3)外部业务变量(开发者定义的 ThreadLocal 变量)](#(3)外部业务变量(开发者定义的 ThreadLocal 变量))
- (4)结构关系
- 【2】引用链路
-
- (1)正常使用时:完整引用链路(分两条)
- [(2)断开外部 ThreadLocal 引用:tl = null 后的引用链路](#(2)断开外部 ThreadLocal 引用:tl = null 后的引用链路)
-
- [1. GC 发生后的变化](#1. GC 发生后的变化)
- [2. 两种分支情况](#2. 两种分支情况)
- [(3)执行 threadLocal.remove () 后的引用链路(彻底释放)](#(3)执行 threadLocal.remove () 后的引用链路(彻底释放))
- 【3】设计思路
-
- [(1)为什么 Key 要设计成弱引用?](#(1)为什么 Key 要设计成弱引用?)
- [(2)为什么 Value 必须是强引用,不能用弱引用?](#(2)为什么 Value 必须是强引用,不能用弱引用?)
- [(3)这套引用设计天生存在的缺陷:Value 内存泄漏](#(3)这套引用设计天生存在的缺陷:Value 内存泄漏)
- [(4)ThreadLocalMap 自带的自动清理机制(兜底方案)](#(4)ThreadLocalMap 自带的自动清理机制(兜底方案))
- [(5)官方推荐的兜底方案:手动 remove ()](#(5)官方推荐的兜底方案:手动 remove ())
- 【4】整套引用设计思路总结(分层梳理)
- 【5】开发对应注意事项(结合引用设计衍生)
【一】引用分类
Java 从 JDK1.2 开始引入引用分级机制 ,目的是精细化控制对象回收时机,解决传统强引用无法灵活释放内存、缓存溢出、大对象内存泄漏等问题。
共分为 4 类,强度从高到低:强引用 > 软引用 > 弱引用 > 虚引用。
【1】强引用 Strong Reference(默认引用)
(1)定义与作用
代码中最普通的对象赋值,只要存在强引用链,GC 永远不会回收该对象,OOM 也不会释放。
- 作用:正常业务对象持有,保证核心业务对象存活;
- 回收规则:只有所有强引用断开(置为 null、跳出作用域),GC 才会回收。
(2)代码案例
java
public class StrongRefDemo {
public static void main(String[] args) {
// 强引用:obj 持有对象
Object obj = new byte[1024 * 1024 * 10];
System.gc();
// GC 后对象依旧存在,强引用不会被回收
System.out.println(obj);
// 断开强引用链
obj = null;
System.gc();
// 无任何强引用,下次GC直接回收堆内存
}
}
(3)强引用的场景
只要一条可达的强引用链指向堆对象,就是强引用,GC 不会回收,下面分大类列举所有开发中会遇到的强引用场景,并配示例。
1-普通局部变量引用(方法内)
方法中直接 new 赋值给变量,作用域内全程强引用。
java
void test() {
// str 是强引用
String str = new String("demo");
byte[] big = new byte[1024*1024];
}
- 生命周期:方法执行期间有效;
- 释放时机:方法执行完毕,局部变量栈帧销毁,引用消失;
- 手动提前释放:
str = null;
2-成员变量(实例变量)
对象内部属性持有另一个对象,只要实例本身可达,属性就是强引用。
java
class User {
// 实例成员,强引用
List<Order> orderList = new ArrayList<>();
}
User u = new User(); // u可达 → orderList 强引用存活
释放条件:u = null,整个 User 对象不可达,内部成员引用才失效。
3-静态变量 /static 静态集合(最容易内存泄漏)
static 属于类,类加载后常驻方法区,只要类不卸载,引用永久有效。
java
// 全局静态缓存,强引用永久持有
public static List<Object> CACHE = new ArrayList<>();
// 静态对象
public static BigData DATA = new BigData();
风险:放入大量大对象,程序运行期间永远不会回收,极易 OOM。
4-数组中存储对象
数组元素对内部对象都是强引用。
java
Object[] arr = new Object[10];
arr[0] = new byte[1024*1024]; // 数组持有,强引用
只有数组本身失去所有强引用,内部元素才会被释放。
5-容器集合(ArrayList/HashMap/HashSet 等)
所有普通集合内部存储都是强引用,key、value 全部强持有。
java
HashMap<String, Object> map = new HashMap<>();
map.put("k", new LargeImg()); // key、value 均为强引用
对比:WeakHashMap 只有 key 是弱引用,value 依旧是强引用。
6-ThreadLocal 存储的值(极易内存泄漏)
ThreadLocalMap 的 key 是弱引用,但 value 是强引用。
java
ThreadLocal<BigFile> tl = new ThreadLocal<>();
tl.set(new BigFile());
线程池场景下线程复用,不调用 tl.remove(),value 会一直强引用常驻堆。
7-方法参数、返回值传递
对象作为入参、返回值,调用栈持有强引用。
java
// arg 是强引用
void func(Object arg) {}
Object getObj() {
return new Object(); // 返回后接收变量持有强引用
}
8-内部类 / 匿名内部类 / Lambda 持有外部对象
非静态内部类会隐式持有外部类实例的强引用;Lambda 捕获外部变量也会生成强引用。
java
class Outer {
List list = new ArrayList();
// 非静态内部类隐式持有 Outer.this 强引用
class Inner {}
}
// Lambda 捕获外层变量,产生强引用
Runnable run = () -> System.out.println(list.size());
容易出现:外部类本应回收,但内部类 / Lambda 还在运行,导致外部类无法释放。
9-循环引用(A 持有 B,B 持有 A)
两个对象互相持有对方,属于双向强引用链。
现代 CMS/G1/ZGC 可达性分析可识别并回收,但仍会增加 GC 开销。
class A { B b; }
class B { A a; }
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
// 无外部强引用,GC 可回收
10-本地变量缓存、临时引用赋值
多次赋值只要变量还在,就是强引用:
java
Object o1 = new Object();
Object o2 = o1; // o2 也是同对象的强引用
11-JNI / 本地方法持有 Java 对象
native 代码通过 JNI 保存全局引用,会长期强持有 Java 堆对象,不主动释放会内存泄漏。
12-常量池、字符串常量引用
java
// 常量池常驻,强引用永久存在
String s = "abc";
如果把常量字符串作为 WeakHashMap 的 key,常量池一直持有 key,key 永远不会被回收,弱引用失效。
(4)快速总结:哪些属于强引用
- 普通局部变量、实例成员变量
- static 静态变量、静态集合
- 数组、HashMap/ArrayList 等普通容器的 key/value
- ThreadLocal 的 value
- 内部类、匿名类、Lambda 捕获外部对象
- 方法参数、返回值接收对象
- 对象互相循环引用
- JNI 全局引用、字符串常量池对象
(5)开发注意事项
- 静态集合极易内存泄漏
static List<Object> cache = new ArrayList<>()全局静态集合持有对象,程序不退出永远不回收,大量缓存直接 OOM; - 局部变量及时置空:方法内超大数组、大文件对象,使用完手动
xxx=null缩短引用生命周期; - 避免长生命周期对象持有短期大对象(比如全局缓存持有图片、文件字节数组);
- ThreadLocal 用完必须
remove(),否则线程复用导致强引用常驻堆。
【2】软引用 SoftReference(缓存专用)
(1)定义与作用
强度次于强引用,内存充足时 GC 不回收;内存不足、即将发生 OOM 前,JVM 会自动回收软引用对象 。
搭配 ReferenceQueue 可监听回收事件。
- 核心场景:内存缓存(图片缓存、本地资源缓存、本地二级缓存);
- 回收规则:堆空闲内存充足 → 保留;堆内存紧张 → 全部回收。
(2)代码案例
java
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
public class SoftRefDemo {
public static void main(String[] args) {
// 引用队列,对象被回收后会入队
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
// 软引用包装大数组
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024 * 20], queue);
System.out.println("内存充足,获取对象:" + softRef.get());
// 疯狂分配内存,挤压堆空间触发软引用回收
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024 * 10]);
}
// 内存耗尽前 softRef.get() 返回 null,对象已被回收
}
}
(3)开发注意事项
- 做本地缓存优先用
SoftReference,替代单纯HashMap,自动控内存; - 必须配合
ReferenceQueue清理失效软引用,否则 Reference 对象本身堆积内存泄漏; - 高并发缓存场景建议封装工具类,定期清理队列中已回收的软引用 Key;
- 不能用于必须常驻的业务数据(内存紧张会丢失缓存,业务需做好缓存击穿兜底);
- JVM 参数可调整软引用回收策略:
-XX:SoftRefLRUPolicyMSPerMB,控制空闲内存保留时长。
【3】弱引用 WeakReference(临时关联、无强制存活)
(1)定义与作用
强度低于软引用,只要发生 GC,无论内存是否充足,直接回收弱引用对象。
- 核心场景:
WeakHashMap(底层全是弱引用)、临时监听、非强制缓存、关联元数据; - 典型使用:ThreadLocalMap、缓存元信息、避免强引用循环泄漏。
(2)代码案例
java
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.out.println("GC前:" + weakRef.get());
System.gc(); // 主动触发GC
System.out.println("GC后:" + weakRef.get()); // null,对象已回收
}
}
经典场景:WeakHashMap
java
// key 是弱引用,key无外部强引用时自动清除Entry
WeakHashMap<String, Object> weakMap = new WeakHashMap<>();
String key = new String("cache-key");
weakMap.put(key, new byte[1024 * 1024]);
key = null; // 断开强引用
System.gc();
// map 自动清除该键值对,不会常驻内存
(3)开发注意事项
WeakHashMapKey 必须是包装对象,不能是常量字符串(字符串常量池存在强引用,不会回收);- 弱引用对象回收不可控,不能存储需要稳定读取的数据;
- ThreadLocal 底层使用弱引用 key,若线程不清理 value 仍会发生内存泄漏(value 是强引用);
- 大量临时元数据、一次性缓存优先弱引用,减少堆常驻对象。
【4】虚引用 PhantomReference(最弱,仅用于堆外内存回收)
(1)定义与作用
强度最低,无法通过 get () 获取原始对象 ,唯一作用:对象被 GC 回收时,收到回收通知,用于资源清理。
必须绑定 ReferenceQueue,无队列则无任何意义。
- 核心场景:堆外内存(NIO DirectBuffer)释放、文件句柄、Native 资源、自定义资源回收;
- 回收规则:对象进入可达性分析不可达后,放入队列,开发者在队列中做资源释放。
(2)代码案例
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomRefDemo {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);
System.out.println(phantom.get()); // 永远返回null,无法获取对象
obj = null;
System.gc();
// 阻塞等待对象回收通知
Reference<?> ref = queue.remove();
System.out.println("对象已被GC,可以释放底层native资源");
}
}
底层 NIO DirectByteBuffer 就是依靠虚引用监控堆外内存,对象回收时主动释放操作系统堆外内存,避免堆外内存溢出。
(3)开发注意事项
- 业务代码极少手动使用,JDK NIO、文件流底层封装;
- 虚引用不能持有业务对象,仅做回收钩子;
- 队列处理线程要异步、低延迟,避免阻塞 GC 回收链路;
- 禁止在虚引用回调中创建新强引用,会导致对象复活、永久无法回收。
【二】四种引用对比总表
表格
| 引用类型 | 回收时机 | 核心用途 | get () 是否返回对象 |
|---|---|---|---|
| 强引用 | 无任何强引用链才回收 | 正常业务对象、核心数据 | 一定返回 |
| 软引用 | 内存不足 OOM 前回收 | 内存缓存、图片资源 | 内存充足返回,不足 null |
| 弱引用 | 只要 GC 就回收 | WeakHashMap、临时元数据 | GC 后返回 null |
| 虚引用 | GC 标记后入队,无法获取对象 | 堆外 / Native 资源释放 | 永远 null |
【三】通用开发规范与避坑总结
【1】内存缓存选型规范
- 永久不能丢的数据:强引用 + 持久化;
- 可丢失、内存友好缓存:
SoftReference; - 临时、无强依赖元数据:
WeakReference; - 堆外 / 本地文件 / 原生资源:依赖虚引用做后置清理。
【2】通用内存泄漏风险点
- 静态集合强持有大对象,无过期清理;
- ThreadLocal 使用后不 remove,线程池复用导致 value 常驻;
- 缓存未使用软 / 弱引用,无限膨胀 OOM;
- 堆外 DirectBuffer 未被正常回收,虚引用线程阻塞导致堆外溢出;
- 循环强引用(A 持有 B,B 持有 A),无外部引用时现代 GC 可回收,但老版本会泄漏;
- ReferenceQueue 不消费,大量 Soft/WeakReference 实例堆积占用堆。
【3】GC 与引用开发最佳实践
- 缓存工具类统一封装软引用,定时轮询 ReferenceQueue 清理失效引用;
- 大对象使用完毕手动置空,缩短强引用生命周期;
- 线程池、ThreadLocal 遵循用完即清原则;
- 堆外内存场景尽量使用池化,减少虚引用回收压力;
- 不依赖
System.gc()强制回收,仅做调试,生产禁用; - 弱引用 Key 避免常量池字符串、全局单例对象。
【4】问题延伸
WeakHashMap为什么 Key 弱引用、Value 强引用?
防止 value 反向强引用 key,导致 key 无法被回收;- 软引用和弱引用的使用场景区分:
软引用适合用户可容忍缓存丢失的资源;弱引用适合生命周期跟随 key 的附属数据; - 虚引用为什么不能 get 对象?
此时对象已完成标记清除,内存随时会被回收,不允许访问防止野指针。
【四】ThreadLocal的引用分析案例
【1】底层存储结构前置认知
(1)Thread 线程对象
每个 Thread 实例持有成员变量 threadLocals ,ThreadLocal 本身不存数据,数据存在当前线程 Thread 对象内部:
java
// Thread 类源码
ThreadLocal.ThreadLocalMap threadLocals = null;
- 生命周期:线程创建时初始化,线程销毁后整个
threadLocals直接丢弃; - 线程池场景:线程长期存活,
threadLocals不会被销毁。
(2)ThreadLocalMap(自定义哈希表,非 HashMap)
ThreadLocalMap 是定制哈希表,内部存储数组 Entry[] table,核心存储单元是自定义 Entry。
Entry 自定义实现:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 交给父类 WeakReference 包装
value = v; // value 直接强引用保存
}
}
核心设计:
- Entry 的 key = WeakReference(弱引用)
- Entry 的 value = 普通强引用 Object
(3)外部业务变量(开发者定义的 ThreadLocal 变量)
java
// 外部强引用:tl 是栈上局部变量 / static静态变量
ThreadLocal<User> tl = new ThreadLocal<>();
tl.set(new User());
(4)结构关系
thread------》ThreadLocalMap------》Entry数组------》key(是threadLocal的弱引用)、value(存入的Object对象new User())

【2】引用链路
(1)正常使用时:完整引用链路(分两条)
(1)链路 1:外部代码 → ThreadLocal 对象(Key 本体)【强引用链】
bash
栈局部变量 tl(强引用) → ThreadLocal实例(key本体)
只要开发者没有执行 tl = null,这条强引用链一直存在。
(2)链路 2:Thread 线程 → ThreadLocalMap → Entry → Key+Value 混合引用链
bash
Thread线程对象(强引用)
↓
threadLocals(ThreadLocalMap,强引用成员变量)
↓
Entry[] table 数组(强引用持有每一个Entry)
↓
Entry 对象
├─ 父类WeakReference<ThreadLocal> → 弱引用指向 ThreadLocal实例(Key)
└─ 字段 value → 强引用指向 业务数据对象(Value)
(3)合并完整可达链(正常场景)
bash
线程Thread → ThreadLocalMap → Entry
弱引用 → ThreadLocal(Key) ← 外部变量tl(强引用)
强引用 → 业务对象User(Value)
此时:
- Key(ThreadLocal)同时存在外部强引用 + Entry 内弱引用,GC 绝对不会回收;
- Value 只有一条强引用链:
Thread -> Map -> Entry -> value,线程存活则 Value 永远存活。
(2)断开外部 ThreadLocal 引用:tl = null 后的引用链路
执行代码:tl = null;
此时外部栈强引用链断裂,只剩下 Entry 内部的弱引用指向 ThreadLocal Key:
bash
Thread → ThreadLocalMap → Entry
├─ 弱引用 → ThreadLocal(Key) 【无任何强引用了】
└─ 强引用 → User(Value)
1. GC 发生后的变化
因为 Key(ThreadLocal)只剩弱引用,GC 会直接回收 ThreadLocal 实例;
此时 entry.get() 返回 null,该 Entry 变成空 key 残留 Entry:
bash
Thread → ThreadLocalMap → Entry
├─ 弱引用:目标已被回收,get()=null
└─ 强引用 → User(Value) 依然存在!
2. 两种分支情况
(1)分支 A:线程后续继续调用 get/set/rehash
ThreadLocalMap 在读写时会执行 expungeStaleEntry() 探测清理:
发现 entry.get() == null,手动执行:
java
entry.key = null;
entry.value = null;
断开 Value 的强引用,业务对象 User 失去引用链,下一次 GC 回收。
(2)分支 B:线程池线程长期不再操作该 ThreadLocalMap(最容易泄漏)
线程长期存活,不再执行任何 get/set,不会触发自动清理;
残留 Entry 永久存在,Value 的强引用链永远无法断开:
bash
Thread -> ThreadLocalMap -> Entry -> value(强引用) -> User对象
User 对象无法被 GC,产生内存泄漏。
(3)执行 threadLocal.remove () 后的引用链路(彻底释放)
remove() 会直接定位当前 ThreadLocal 对应的 Entry,做两步清空:
- Entry 的弱引用 key 置空;
- Entry 的 value 字段置空;
引用链完全断裂:
Thread -> ThreadLocalMap -> Entry(key=null,value=null)
Key、Value 都无任何引用,GC 可一次性回收,从根源杜绝泄漏。
【3】设计思路
(1)为什么 Key 要设计成弱引用?
(1)场景推演:没有弱引用会发生严重内存泄漏
假设 key 是强引用:
- 业务代码定义
ThreadLocal tl = new ThreadLocal<>(); - 线程调用
tl.set(obj),ThreadLocalMap.Entry强持有tl - 业务代码断开外部引用:
tl = null; - 此时 Entry 内部还存在一条强引用链:
Thread -> threadLocals -> Entry -> key(强引用) -> tl对象 tl永远无法被 GC,Entry 永久残留在线程 map 中,value 也跟着常驻堆
(2)弱引用的解决方案
key 被 WeakReference 包装:
- 外部
tl = null后,不存在任何强引用指向 ThreadLocal 实例 - 下一次 GC 会直接回收 ThreadLocal 对象
- 当
ThreadLocalMap扩容、set、get 操作扫描哈希槽时,会发现entry.get() == null(key 已回收),自动清空整条 Entry(key+value),释放内存
(3)设计目的总结
让 ThreadLocal 对象本身能正常被垃圾回收,避免 ThreadLocal 实例永久驻留在线程的 Map 里,降低无手动清理时的内存泄漏概率。
(2)为什么 Value 必须是强引用,不能用弱引用?
很多人疑惑:既然 key 用弱引用,value 为什么不一起弱引用?
(1)业务逻辑层面:value 是我们要存储的数据
使用 ThreadLocal 的核心诉求:在线程生命周期内持有数据 。
如果 value 是弱引用:
- 线程执行中途只要触发一次 GC,value 直接被回收
get()突然返回 null,业务代码无感知,出现诡异空指针、上下文丢失,完全不符合线程隔离存储的设计目标。
(2)生命周期绑定逻辑
- key(ThreadLocal):工具对象,用完可丢弃,允许 GC 回收
- value(业务数据):线程执行期间必须稳定存在,需要强引用保活
(3)反向引用风险
如果 value 弱引用,同时 key 弱引用:
线程执行中 GC 随时清空 value,上下文直接丢失,违背 ThreadLocal 线程私有存储的定位。
(3)这套引用设计天生存在的缺陷:Value 内存泄漏
(1)完整泄漏链路(线程池场景最严重)
- 线程池核心线程长期复用,线程对象不会销毁
ThreadLocal tl = new ThreadLocal<>(); tl.set(大对象);- 业务代码执行完:
tl = null; - GC 回收 ThreadLocal(key 弱引用生效),Entry 变成
[key=null, value=大对象] - 若该线程后续不再执行 get/set/remove,ThreadLocalMap 不会自动清理空 key 的 Entry
- 线程长期存活,Entry 常驻,value 强引用无法释放 → 内存泄漏
(2)触发条件
- 使用线程池(线程不销毁)
- ThreadLocal 实例外部引用置空,没有手动
remove() - 该线程后续不再操作这个 ThreadLocalMap,自动清理机制无法触发
(4)ThreadLocalMap 自带的自动清理机制(兜底方案)
源码中 get() / set() / rehash() 方法都会执行探测清理 expungeStaleEntry :
遍历哈希桶,遇到 entry.get() == null 的过期 Entry:
- 将 entry.key = null
- 将 entry.value = null(断开 value 强引用)
- 整个 Entry 置空,帮助 GC 回收
局限性:
只有访问 ThreadLocalMap 时才会清理;如果线程休眠、阻塞,长期不操作 map,过期 Entry 会持续堆积。
(5)官方推荐的兜底方案:手动 remove ()
无论强弱引用设计,规范写法:
java
try {
threadLocal.set(context);
// 业务逻辑
} finally {
threadLocal.remove(); // 主动删除当前Entry,彻底断开key、value引用
}
执行 remove 会直接把对应 Entry 的 key、value 置空,从根源杜绝泄漏,不依赖 GC 和自动清理逻辑。
【4】整套引用设计思路总结(分层梳理)
(1)设计目标
- 实现线程私有数据隔离;
- 允许 ThreadLocal 工具对象正常 GC,不常驻内存;
- 保证线程运行期间存储的业务数据不被 GC 随意回收;
- 内置自动清理逻辑作为兜底,缓解内存泄漏。
(2)分层设计取舍
- Key 使用弱引用
解决 ThreadLocal 对象本身无法回收的问题,避免工具类对象永久占用 Entry; - Value 使用强引用
保障线程上下文数据稳定存活,防止 GC 随机清空业务数据; - 内置过期 Entry 自动清理
在读写 map 时主动清除 key 已回收的无效 Entry,释放 value 强引用; - 暴露 remove API
给开发者提供主动释放手段,解决线程池长期线程导致的堆积泄漏问题。
【5】开发对应注意事项(结合引用设计衍生)
- 线程池场景必须在 finally 执行 remove,不能依赖弱引用自动清理;
- 不要在线程中存放超大对象(大字节数组、大量缓存),泄漏后内存占用极高;
- 不要将 ThreadLocal 定义为局部临时变量且不 remove,极易产生过期 Entry;
- 若使用一次性短期线程(无线程池,执行完销毁),线程对象回收时整个 ThreadLocalMap 直接释放,泄漏风险极低;
- 弱引用仅解决 ThreadLocal 对象回收,完全解决不了 value 泄漏,不要误以为弱引用就能高枕无忧。