Lesson 50 · Java 高级特性
四种引用类型:强引用、软引用、弱引用、虚引用
强引用:Java 中最常见的引用,也是 OOM 的元凶
强引用(Strong Reference)是 Java 中最基本的引用类型——你写的几乎每一行代码中的引用都是强引用。它的特性只有一个:只要强引用存在,GC 绝不回收对象。
public class StrongRefDemo {
public static void main(String[] args) {
Object obj = new Object(); // obj 是强引用
// 即使内存不够,GC 也不会回收 obj 指向的对象
// 宁可抛出 OOM,也不会回收强引用对象
obj = null; // ★ 显式设为 null 才能断开引用
// 此时对象变为不可达,可以被 GC 回收
}
}
// 常见的强引用泄漏场景:
// ① 集合中对象忘记 remove
List<Object> cache = new ArrayList<>();
cache.add(bigObject); // cache 强引用 bigObject → 永不回收
// ② 静态集合
static Map<String, Object> globalMap = new HashMap<>();
globalMap.put("key", bigObject); // 静态变量生命周期=JVM 生命周期
// ③ ThreadLocal 中的 Value(见 ThreadLocal 那篇)
从 GC Roots 出发,通过强引用链能到达的对象 = 存活对象。
只有当一个对象没有任何强引用指向它时,才可能被 GC 回收。
软引用 SoftReference:内存敏感的缓存利器
软引用(Soft Reference)的特性:内存充足时不回收,内存不足时回收。这个特性使它非常适合用来实现内存敏感的缓存。
public class ImageCache {
// key=图片路径, value=图片对象的软引用
private Map<String, SoftReference<Image>> cache = new HashMap<>();
public Image getImage(String path) {
SoftReference<Image> ref = cache.get(path);
if (ref != null) {
Image img = ref.get(); // ★ 可能返回 null(已被 GC 回收)
if (img != null) {
return img; // 缓存命中
}
}
// 缓存未命中或已被回收,重新加载
Image img = loadImageFromDisk(path);
cache.put(path, new SoftReference<>(img));
return img;
}
}
/*
* GC 行为:
* - 内存充足 → SoftReference 指向的对象不会被回收
* - 内存紧张 → GC 会回收 SoftReference 指向的对象(在抛出 OOM 之前)
*
* JDK 回收策略(近似):
* 软引用对象的存活时间 < (JVM 空闲内存 MB)
* 即空闲内存越多,软引用存活越久
*/
JVM 规范只说"在即将发生 OOM 之前回收软引用"。具体实现因 GC 收集器不同:
-XX:SoftRefLRUPolicyMSPerMB(默认 1000ms)控制软引用的存活策略:
如果堆的空闲内存是 10MB,则软引用对象最多存活 10 × 1000 = 10 秒。超过这个时间就会在下一次 GC 时被回收。空闲内存越小,回收越积极。
坑 1:GC 回收时机不可控。内存充足时可能一直不回收(浪费内存),内存紧张时突然全部回收(缓存击穿)。
坑 2:SoftReference 对象本身需要内存开销(每个约 40 字节)。
坑 3:现代生产环境更推荐 LRU Cache(如 Guava Cache、Caffeine),淘汰策略可控、可观测。
结论:SoftReference 适合做"锦上添花"的二级缓存,不适合作为核心缓存方案。
弱引用 WeakReference:下次 GC 必定回收
弱引用(Weak Reference)的特性:无论内存是否充足,只要发生 GC,弱引用指向的对象就会被回收。这使得弱引用适合做"不希望阻止对象被回收"的关联映射。
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
System.out.println(weakRef.get()); // java.lang.Object@xxx
obj = null; // 断开强引用
System.gc(); // 建议 GC
System.out.println(weakRef.get()); // null ★ 对象已被回收!
// 对比软引用:
Object obj2 = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj2);
obj2 = null;
System.gc();
System.out.println(softRef.get()); // 仍然有值(内存充足不回收)
弱引用最经典的应用是 WeakHashMap 和 ThreadLocal 的 Entry。
WeakHashMap<Object, String> map = new WeakHashMap<>();
Object key1 = new Object();
Object key2 = new Object();
map.put(key1, "value1");
map.put(key2, "value2");
System.out.println(map.size()); // 2
key1 = null; // 断开 key1 的强引用
System.gc();
// ★ key1 对应的 Entry 被自动移除!
System.out.println(map.size()); // 1
/*
* WeakHashMap 原理:
* - Entry 继承 WeakReference,key 是弱引用
* - 当 key 不再被外部强引用时,GC 回收 key
* - WeakHashMap 在下次操作时,通过 ReferenceQueue 感知到 key 被回收
* - 自动移除对应的 Entry(包括 value)
*/
ThreadLocalMap 的 Entry 继承了 WeakReference,其 key(ThreadLocal 实例)是弱引用。这样当外部不再持有 ThreadLocal 变量时,ThreadLocal 实例本身可以被 GC 回收。
但这只解决了"ThreadLocal 对象泄漏"的一半问题——Entry 中的 value 是强引用,如果线程不销毁,value 仍然泄漏。这就是 ThreadLocal 内存泄漏的核心矛盾(详见 ThreadLocal 专题)。
虚引用 PhantomReference:对象死亡前的最后通知
虚引用是四种引用中最特殊的一种:它不影响对象的生命周期,也无法通过 get() 获取对象。它的唯一作用是在对象被 GC 回收时,将通知发送到关联的 ReferenceQueue。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);
// ★ get() 永远返回 null!虚引用无法获取对象
System.out.println(phantom.get()); // null
obj = null; // 断开强引用
System.gc();
// GC 回收对象后,PhantomReference 被放入队列
Reference<?> ref = queue.poll();
System.out.println(ref == phantom); // true ★ 确认对象已被回收
/*
* 虚引用的设计意图:
* ① 无法通过虚引用获取对象 → 不会"意外复活"对象
* ② 对象被回收时收到通知 → 可以做清理工作(释放堆外内存等)
* ③ 比 finalize() 更安全 → 不会导致对象复活或性能问题
*/
虚引用最典型的真实应用是 JDK 9+ 的 Cleaner 机制和 DirectByteBuffer 的清理。
| 特性 | finalize() | PhantomReference + Cleaner |
|---|---|---|
| 复活对象 | 可以在 finalize() 中重新建立强引用(危险!) | 不可能——get() 永远返回 null |
| 执行线程 | Finalizer 线程(单线程,可能阻塞) | Cleaner 专用线程池 |
| 异常处理 | 异常被吞掉,后续 finalize 不再执行 | 异常不影响其他清理任务 |
| 性能影响 | 对象至少多存活一个 GC 周期 | 无额外 GC 开销 |
| JDK 态度 | JDK 9 起标记为 @Deprecated | 官方推荐的替代方案 |
finalize() 有三个致命问题:
1. 对象复活:在 finalize() 中可以 SomeClass.instance = this; 将自身赋给静态变量,对象重新可达——但 finalize() 只会调用一次,第二次 GC 直接回收,不做清理。
2. 性能灾难:需要 finalize 的对象会被放入 Finalizer 队列,由单个 Finalizer 线程依次执行。如果 finalize() 执行慢,整个回收流程被阻塞,对象堆积导致 OOM。
3. 不确定性:JVM 不保证 finalize() 何时被调用,甚至不保证会被调用(程序退出时可能跳过)。依赖 finalize() 释放资源是极其危险的。
import java.lang.ref.Cleaner;
public class ResourceHolder implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// ★ 清理逻辑必须放在独立的静态内部类中
// 不能持有外部类的引用(否则阻止回收)
private static class CleanAction implements Runnable {
private final long nativeHandle;
CleanAction(long handle) { this.nativeHandle = handle; }
@Override
public void run() {
// ★ 对象被 GC 后,Cleaner 会调用此方法
freeNativeResource(nativeHandle);
}
}
private final Cleaner.Cleanable cleanable;
public ResourceHolder(long handle) {
// 注册清理动作:当 this 被 GC 时,执行 CleanAction
this.cleanable = cleaner.register(this, new CleanAction(handle));
}
@Override
public void close() {
cleanable.clean(); // 主动清理(不必等 GC)
}
}
/*
* Cleaner 底层使用 PhantomReference + ReferenceQueue
* JDK 内部 DirectByteBuffer 的 Cleaner 就是这样实现的:
* 当 DirectByteBuffer 被 GC 时,Cleaner 负责释放堆外内存
*/
ReferenceQueue:引用回收的通知机制
ReferenceQueue 是软引用、弱引用、虚引用的"回收通知队列"。当引用指向的对象被 GC 回收后,引用本身会被加入队列,应用程序可以从队列中获取通知并做清理。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Map<WeakReference<Object>, String> metadata = new HashMap<>();
// 创建对象并注册弱引用
for (int i = 0; i < 5; i++) {
Object obj = new Object();
WeakReference<Object> ref = new WeakReference<>(obj, queue);
metadata.put(ref, "metadata-" + i);
// obj 局部变量,循环结束后无强引用
}
System.gc();
// ★ 从队列中取出被回收的引用,做清理
Reference<? extends Object> ref;
while ((ref = queue.poll()) != null) {
String meta = metadata.remove(ref);
System.out.println("清理元数据: " + meta);
}
/*
* 各引用类型的入队时机:
* 软引用:对象被 GC 回收后入队
* 弱引用:对象被 GC 回收后入队
* 虚引用:★ 对象被 GC 回收后,从 Finalizer 队列移除后才入队
* (即对象真的"死透了"才入队,不像 finalize 可能复活对象)
*/
| 引用类型 | get() 返回值 | 必须配合 Queue? | 入队时机 |
|---|---|---|---|
| 强引用 | N/A(不适用) | N/A | N/A |
| 软引用 | 存活对象 或 null | 可选 | 对象被回收后 |
| 弱引用 | 存活对象 或 null | 可选 | 对象被回收后 |
| 虚引用 | 永远返回 null | 必须 | 对象 finalize 后入队 |
总结:四种引用全景对比与使用建议
| 引用类型 | GC 回收条件 | 典型用途 | get() 返回值 | 生产案例 |
|---|---|---|---|---|
| 强引用 | 永不回收(只要引用存在) | 一切正常业务对象 | N/A | 所有 Java 代码 |
| 软引用 | 内存不足时回收 | 内存敏感的缓存 | 对象 / null | Android BitmapCache |
| 弱引用 | 下次 GC 必定回收 | 关联映射、防泄漏 | 对象 / null | WeakHashMap、ThreadLocal |
| 虚引用 | 随时回收(等同无引用) | 堆外内存清理 | 永远 null | DirectByteBuffer Cleaner |
- "四种引用的区别?"——按 GC 回收的"积极程度"排序:强引用(永不) > 软引用(内存不足时) > 弱引用(下次 GC) > 虚引用(随时)
- "软引用做缓存靠谱吗?"——不够可控,回收时机取决于 JVM 内存状态。生产推荐 Caffeine / Guava Cache
- "WeakHashMap 和 HashMap 的区别?"——WeakHashMap 的 key 是弱引用,当 key 无外部强引用时,Entry 会被自动清理
- "ThreadLocal 为什么用弱引用?"——Entry 的 key 用弱引用指向 ThreadLocal 实例,当外部不再引用 ThreadLocal 时,实例可被回收。但 value 仍是强引用,必须手动 remove()
- "虚引用有什么用?"——跟踪对象被 GC 的时机,用于替代 finalize() 做资源清理。JDK 的 DirectByteBuffer 用 Cleaner(基于 PhantomReference)释放堆外内存
面试回答模板:
"Java 有四种引用类型。强引用是最常见的,GC 绝不回收。软引用在内存不足时被回收,适合做内存敏感的缓存。弱引用在下次 GC 时必定被回收,用于不希望阻止对象回收的关联场景,典型如 WeakHashMap 和 ThreadLocalMap 的 Entry。虚引用无法获取对象,只在对象被回收时通过 ReferenceQueue 通知,JDK 内部用它实现 Cleaner 来释放 DirectByteBuffer 的堆外内存。"