一篇搞懂 Java 四种引用:ReferenceQueue 实战 + ThreadLocalMap 为何 key 用弱引用
目录
一、先说清:JVM 里的“引用类型”到底在干嘛?
Java 里我们常说的 强引用 / 软引用 / 弱引用 / 虚引用,本质是在告诉 GC:
- 这个对象“有多重要”
- 什么时候允许回收它
- 回收前后我能不能得到通知(ReferenceQueue)
友情提示:System.gc() 不是强制回收,但我们可以用「ReferenceQueue + 重试循环 +(必要时)压内存」让结果非常稳定、肉眼可见。
二、强引用(Strong Reference)
1、定义
最普通的赋值就是强引用:
A a = new A();
只要对象还能从 GC Roots(栈、静态变量、JNI 引用等)沿引用链走到,它就不会被回收。
2、如何“让它能被回收”
把强引用断开即可:
a = null;
3、Demo:看见“对象被回收”的结果(用 finalize 只是演示,生产别用)
finalize() 已被官方不推荐,但用来做“看见回收发生”的演示很直观。
public class StrongRefDemo {
static class A {
private final String name;
A(String name) { this.name = name; }
@Override protected void finalize() throws Throwable {
System.out.println("[finalize] A(" + name + ") 被回收了");
}
}
public static void main(String[] args) throws Exception {
A a = new A("strong");
System.out.println("创建对象: " + a);
a = null; // 断开强引用
System.gc(); // 建议触发回收
Thread.sleep(200); // 给 GC 一点时间
System.out.println("main 结束");
}
}
你通常会看到类似输出(是否立刻打印取决于 JVM,但一般能看到):

更严谨的“可观测”方式是用 WeakReference/ReferenceQueue 来判断对象是否死亡(见后面弱引用 Demo)。
三、软引用(SoftReference)
1、定义
SoftReference<A> ref = new SoftReference<>(new A());
2、回收规则(重点)
- 只有软引用指向对象时:内存充足通常不回收
- 内存紧张时:GC 会倾向回收软引用对象(常用于缓存)
3、Demo:压内存,稳定看到“软引用被清空”
下面 Demo 会不断申请内存,直到软引用被回收(ref.get() == null),并用 ReferenceQueue 看到它入队。
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class SoftRefDemo {
static class A {
final byte[] payload = new byte[5 * 1024 * 1024]; // 5MB
}
public static void main(String[] args) {
ReferenceQueue<A> queue = new ReferenceQueue<>();
SoftReference<A> ref = new SoftReference<>(new A(), queue);
System.out.println("初始 ref.get() = " + ref.get());
// 压内存:不停分配大块数组
List<byte[]> pressure = new ArrayList<>();
try {
for (int i = 1; ; i++) {
pressure.add(new byte[10 * 1024 * 1024]); // 每次 10MB
if (i % 5 == 0) {
System.out.println("已分配约 " + (i * 10) + "MB, ref.get() = " + ref.get());
}
if (ref.get() == null) {
System.out.println("\n✅ 软引用对象已被回收:ref.get() == null");
break;
}
}
} catch (OutOfMemoryError e) {
System.out.println("\n⚠️ 内存压爆了,但 ref.get() 仍为: " + ref.get());
}
Reference<? extends A> polled = queue.poll();
System.out.println("ReferenceQueue 是否收到通知: " + (polled != null));
}
}
运行时你会看到类似:

四、弱引用(WeakReference)
1、定义
WeakReference<A> ref = new WeakReference<>(new A());
2、回收规则(重点)
- 只有弱引用指向对象时:只要发生 GC,就会回收(一般非常快)
- 典型用途:ThreadLocalMap 的 Entry key
3、Demo:稳定看到“弱引用被清空 + 入队”
这个 Demo 基本 99% 都能稳定看到结果。
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class WeakRefDemo {
static class A {
final String name;
A(String name) { this.name = name; }
@Override public String toString() { return "A(" + name + ")"; }
}
public static void main(String[] args) throws Exception {
ReferenceQueue<A> queue = new ReferenceQueue<>();
WeakReference<A> ref = new WeakReference<>(new A("weak"), queue);
System.out.println("GC 前 ref.get() = " + ref.get());
// 多试几次,保证你能“看见”结果
for (int i = 1; i <= 20 && ref.get() != null; i++) {
System.gc();
Thread.sleep(50);
}
System.out.println("GC 后 ref.get() = " + ref.get() + "(null 表示已回收)");
Reference<? extends A> polled = queue.poll();
System.out.println("ReferenceQueue 是否收到通知: " + (polled != null));
}
}

五、虚引用(PhantomReference)
1、定义
虚引用有两个特点:
- 必须配合 ReferenceQueue 使用
get()永远返回null(你拿不到对象)
PhantomReference<A> ref = new PhantomReference<>(new A(), queue);
2、用途(重点)
用来做“对象即将被回收”的通知,典型用法:
Cleaner清理 DirectByteBuffer 的直接内存(非堆内存)- 资源释放回调(更可控的 finalize 替代思路)
3、Demo:看到“虚引用入队”(这就是回收信号)
我们用一个大对象 + 循环触发 GC,等虚引用入队就说明 referent 已不可达并进入回收流程。
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class PhantomRefDemo {
static class A {
final byte[] payload = new byte[20 * 1024 * 1024]; // 20MB
final String name;
A(String name) { this.name = name; }
@Override public String toString() { return "A(" + name + ")"; }
}
public static void main(String[] args) throws Exception {
ReferenceQueue<A> queue = new ReferenceQueue<>();
A a = new A("phantom");
PhantomReference<A> ref = new PhantomReference<>(a, queue);
System.out.println("ref.get() 永远是: " + ref.get());
a = null; // 断开强引用
// 等待入队:入队意味着对象已被判定为可回收(回收阶段会通知)
Reference<? extends A> polled = null;
for (int i = 1; i <= 50 && polled == null; i++) {
System.gc();
Thread.sleep(50);
polled = queue.poll();
}
System.out.println("虚引用是否入队(true 表示回收信号已到): " + (polled != null));
}
}

六、补充:ThreadLocal 为什么用“弱引用”?和强/弱引用有什么关系?
ThreadLocalMap 的 Entry 设计里(概念上):
- key(ThreadLocal)是弱引用
- value 是强引用
这样做的目的之一是:如果 ThreadLocal 这个 key 在外部不再被引用了,GC 可以把 key 回收,避免 key 永远挂在线程里。
但问题来了:
- key 被回收后,Entry 可能变成
key=null - value 仍是强引用,仍然被线程的 ThreadLocalMap 持有
- 线程池线程长期不结束 → value 残留更久
所以你会经常听到一句最佳实践:
ThreadLocal 用完必须 remove():既防止线程池“串台”,也更利于清理残留 value。
七、小结
- 强引用:有强引用就不会回收;断开引用才可能回收
- 软引用:内存够时不回收;内存紧张才回收(适合缓存)
- 弱引用:只要发生 GC 就回收(适合“不要阻止回收”的引用)
- 虚引用:拿不到对象(get 永远 null),配合队列做回收通知(资源清理)
- ThreadLocalMap:key 弱引用、value 强引用 → 务必 remove()
更多推荐
所有评论(0)