一、从一道面试题说起
先来看这段代码——它在一线大厂的面试中出镜率极高:
public class ReferenceDemo {
public static void main(String[] args) {
Object obj = new Object(); // ①
WeakReference<Object> weak = new WeakReference<>(obj); // ②
obj = null; // ③
System.gc(); // ④
System.out.println(weak.get()); // ⑤ 输出 null
}
}
运行结果:null。
为什么 obj 被置空后,WeakReference 就再也拿不到那个对象了?这一切都要从 Java 的四种引用说起。
WeakReference 的生命周期可以用下面的流程图来理解:
flowchart LR
A["new Object()
堆上对象"]:::object
B["obj(强引用)"]:::strong
C["WeakReference(弱引用)"]:::weak
A -->|"② 弱引用指向"| C
A -->|"① 强引用指向"| B
B -.->|"③ obj = null
强引用断开"| X1[" "]
A -.->|"④ GC 触发"| X2["回收对象"]:::gc
classDef object fill:#f9f,stroke:#333,color:#000
classDef strong fill:#ff6b6b,stroke:#c0392b,color:#000
classDef weak fill:#ffd93d,stroke:#f39c12,color:#000
classDef gc fill:#6bcb77,stroke:#27ae60,color:#000
二、强引用——最常见的引用
我们在日常写代码时,99% 的情况用的都是强引用:
User user = new User(); // 强引用
List<String> list = new ArrayList<>(); // 强引用
回收规则: 只要对象存在强引用指向它,GC 永远不会回收它,即使抛出 OOM 也不会。
这意味着下面这段代码就是一颗定时炸弹:
public static final List<Data> CACHE = new ArrayList<>();
while (true) {
CACHE.add(new Data()); // 强引用链:CACHE → ArrayList → Data
// 内存只增不减,直到 OOM
}
经典应用场景: 全局缓存、静态集合、Spring 的 Bean 容器——这些都是强引用在背后支撑。
三、软引用——内存敏感缓存的首选
软引用告诉 GC:”这个对象你看着办,内存够就先留着,不够就收掉。”
// 软引用的典型用法
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
// 获取对象
Bitmap cachedBitmap = softRef.get();
if (cachedBitmap == null) {
// 被 GC 回收了,重新加载
cachedBitmap = loadBitmap();
}
回收规则:
– 内存充足 → 不回收
– 内存紧张,即将 OOM → 回收
– 注意: 不是触发 GC 就回收,而是触发 GC 后内存仍然不够时才回收
软引用的决策逻辑非常清晰:
flowchart TD
Start["软引用指向的对象"] --> Check{"GC 触发"}
Check -->|"当前堆内存充足"| Keep["保留对象
softRef.get() 返回正常"]
Check -->|"当前堆内存不足"| Recycle["回收对象
softRef.get() 返回 null"]
Keep --> End["继续使用缓存"]
Recycle --> Oom["腾出空间,避免 OOM"]
style Keep fill:#6bcb77,stroke:#27ae60,color:#000
style Recycle fill:#ff6b6b,stroke:#c0392b,color:#000
实战:图片缓存系统
public class ImageCache {
private final Map<String, SoftReference<BufferedImage>> cache = new HashMap<>();
public BufferedImage get(String key) {
SoftReference<BufferedImage> ref = cache.get(key);
if (ref != null) {
BufferedImage img = ref.get();
if (img != null) return img;
}
// 缓存失效,从磁盘加载
BufferedImage img = loadFromDisk(key);
cache.put(key, new SoftReference<>(img));
return img;
}
}
为什么这里用软引用而不是 WeakHashMap? 因为图片是我们想尽可能保留的,只有在 JVM 内存告急时才释放——恰好是软引用的设计目标。
四、弱引用——用完即弃
弱引用比软引用 “更软”:只要发生 GC,弱引用指向的对象就会被回收,不管内存够不够。
WeakReference<Object> weak = new WeakReference<>(new Object());
System.gc();
System.out.println(weak.get()); // null,一次 GC 就被回收了
4.1 WeakHashMap
WeakHashMap 是弱引用最经典的应用——它的 Entry 继承自 WeakReference:
Map<Key, Value> map = new WeakHashMap<>();
Key key = new Key("session-1");
map.put(key, "data");
System.out.println(map.size()); // 1
key = null; // 唯一的强引用被切断
System.gc();
System.out.println(map.size()); // 0,自动清理了
核心价值: 当 key 的外部强引用消失后,WeakHashMap 自动删除对应的 Entry,无需手动 remove()。
4.2 ThreadLocal 的内存泄漏之谜
ThreadLocal 是弱引用的另一个重要应用者。看它的内部结构:
// ThreadLocalMap.Entry 的简化源码
static class Entry extends WeakReference<ThreadLocal>> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k); // key 是弱引用!
value = v; // value 是强引用!
}
}
这就解释了经典的内存泄漏场景:
ThreadLocal<Session> tl = new ThreadLocal<>();
tl.set(new Session("admin"));
// 如果忘记 remove()
// tl = null; // ThreadLocal 对象变成弱引用可达,下一轮 GC 被回收
// 但 Entry 中的 value(Session 对象)仍然被强引用着!
// 只要线程存活,value 就永远无法被回收 → 内存泄漏
ThreadLocal 内存泄漏的本质一目了然:
flowchart TB
subgraph "Thread"
T["Thread 对象"] --> Map["ThreadLocalMap"]
Map --> E1["Entry 1
key: WeakReference
value: Session 对象"]
Map --> E2["Entry 2
..."]
end
subgraph "外部"
TL["ThreadLocal tl" ]:::strong
end
TL -- "set()" --> E1
TL -.->|"tl = null 后
弱引用断裂"| Break["Key 被 GC 回收"]
E1 -->|"value 仍是强引用
Session 对象无法回收!"| Leak["☠ 内存泄漏"]:::leak
classDef strong fill:#ff6b6b,stroke:#c0392b,color:#000
classDef leak fill:#e74c3c,stroke:#c0392b,color:#fff
解决方案:
// 务必在 finally 块中清理
ThreadLocal<Session> tl = new ThreadLocal<>();
try {
tl.set(new Session("admin"));
// ... 业务逻辑
} finally {
tl.remove(); // 清除 Entry,避免内存泄漏
}
五、虚引用——最弱的引用
虚引用是四种引用中最弱的——你根本通过它拿不到对象:
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
System.out.println(phantom.get()); // 永远返回 null!
那它有什么用? 当虚引用指向的对象被回收时,JVM 会将这个虚引用入队到 ReferenceQueue 中。这提供了一种 “对象被回收后通知我” 的机制。
经典应用:NIO DirectByteBuffer 的回收
// DirectByteBuffer 内部核心逻辑(简化版)
public class DirectByteBuffer {
private static class Deallocator implements Runnable {
private final long address; // 堆外内存地址
@Override
public void run() {
// unsafe.freeMemory(address); // 释放堆外内存
}
}
private final Cleaner cleaner; // 虚引用的具体实现
DirectByteBuffer(int cap) {
// 分配堆外内存
long address = unsafe.allocateMemory(cap);
cleaner = Cleaner.create(this, new Deallocator(address));
// 当 DirectByteBuffer 对象被回收时,
// Cleaner 会触发 Deallocator 释放堆外内存
}
}
为什么需要这个机制?因为 DirectByteBuffer 本身是堆上的一个小对象(几十字节),但它维护的堆外内存可能是几十 MB。如果只靠 Finalizer 来释放堆外内存,GC 压力巨大,而且 Finalizer 执行时机不确定。通过虚引用 + ReferenceQueue,JVM 可以在对象被回收的同时触发清理动作,既及时又高效。
flowchart LR
subgraph "堆内存"
DBB["DirectByteBuffer 对象
堆上几十字节"]:::heap
end
subgraph "堆外内存"
Off["堆外内存区域
可能几十 MB"]:::offheap
end
DBB -->|"维护引用"| Off
DBB -->|"绑定了虚引用"| Cleaner["Cleaner
PhantomReference 实现"]
DBB -.->|"DBB 被 GC 回收后"| Cleaner
Cleaner -->|"自动触发"| Deallocator["Deallocator.run()
释放堆外内存"]
classDef heap fill:#f9f,stroke:#333,color:#000
classDef offheap fill:#ffd93d,stroke:#f39c12,color:#000
六、四种引用一图对比
| 特性 | 强引用 | 软引用 | 弱引用 | 虚引用 |
|---|---|---|---|---|
| 回收时机 | 永不回收(OOM 也不回收) | 内存不足时回收 | 下一次 GC 就回收 | 任何时候 |
| get() 返回值 | 对象本身 | 对象(可能为 null) | 对象(可能为 null) | 永远为 null |
| 典型用途 | 普通对象引用 | 内存敏感缓存 | WeakHashMap、ThreadLocal | DirectByteBuffer 堆外内存释放 |
| 配合 ReferenceQueue | 不适用 | 回收前入队 | 回收前入队 | 回收后入队 |
回收时机速记口诀:
强永不、软不够、弱下次、虚是 null
七、实战:自己实现一个”可自动清理的缓存”
有了前面的基础,我们可以手写一个既安全又高效的缓存:
public class AutoCleanCache<K, V> {
private final ConcurrentHashMap<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
public void put(K key, V value) {
expungeStaleEntries(); // 清理已被回收的条目
cache.put(key, new SoftReference<>(value, queue));
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
return ref != null ? ref.get() : null;
}
private void expungeStaleEntries() {
Reference extends V> ref;
while ((ref = queue.poll()) != null) {
// 从 cache 中移除对应的 key
cache.entrySet().removeIf(entry -> entry.getValue() == ref);
}
}
}
工作原理:
1. 使用 SoftReference 包装 value,内存紧张时自动回收
2. ReferenceQueue 监控哪些 value 被回收了
3. 每次 put 时顺带清理一次,不引入额外线程
总结
- 强引用:默认引用类型,GC 永不回收,OOM 风险需自行管理
- 软引用:内存敏感缓存的利器,内存充足时保留,不足时释放
- 弱引用:一次 GC 就回收,WeakHashMap 和 ThreadLocal 是其代表,但 ThreadLocal 用完后务必 remove()
- 虚引用:get() 永远 null,用于精准监听对象回收事件,NIO 堆外内存依赖它
四种引用是理解 JVM 内存回收机制的钥匙。掌握了它们,你不仅能写出更好的代码,面试时遇到”你对 Java 引用了解多少”这类问题,也能从源码到实战讲得清清楚楚。


暂无评论内容