痛点引入: 为什么需要不同的引用类型?直接只用强引用不行吗?(内存泄漏风险、缓存管理粗粒度、对象生命周期监听需求)
核心作用: 解释引用类型如何让程序员与垃圾收集器(GC)协作,更精细地控制对象的生命周期,影响GC行为。
可达性分析算法(GC Roots)是GC判断对象是否存活的基础。
对象从创建到被GC回收的生命周期(强可达 -> ... -> 不可达 -> 回收)。
核心概念: 引用类型直接影响对象在可达性分析链条中的“强度”,从而决定GC何时可以回收该对象。
定义: 最常见的引用类型,通过new
关键字创建的对象默认就是强引用。
语法: Object obj = new Object();
(obj
就是一个指向新创建Object
实例的强引用)
特点:
最强引用: 只要强引用存在(即通过obj
能访问到该对象),GC就绝对不会回收这个对象。
内存泄漏根源: 无意中保持的强引用(如静态集合长期持有对象、监听器未注销)是导致内存泄漏最常见的原因。
如何中断: 显式地将引用设置为null
(obj = null;
),或者让引用超出作用域。之后对象变得可被GC回收(但非立即回收)。
定义: 用来描述一些还有用但并非必需的对象。
核心类: java.lang.ref.SoftReference
语法:
MyExpensiveObject strongRef = new MyExpensiveObject(); // 强引用创建对象
SoftReference softRef = new SoftReference<>(strongRef);
strongRef = null; // 去掉强引用,只剩软引用
// 稍后尝试获取
MyExpensiveObject retrieved = (MyExpensiveObject) softRef.get();
if (retrieved == null) {
// 对象已被GC回收,需要重新创建或加载
}
GC行为:
在内存充足时,GC不会回收仅被软引用指向的对象。
当JVM面临内存不足(即将发生OOM) 时,GC会尝试回收这些仅被软引用指向的对象。
回收发生在Full GC之前(通常是临近OOM时)。
特点:
内存敏感缓存的理想选择: 非常适合实现内存敏感的缓存(如图片缓存、临时计算结果缓存)。缓存的对象在内存吃紧时会被自动释放,避免OOM;内存充足时又能提高性能。
get()
方法: 可能返回null
(如果对象已被回收),使用前需检查。
使用场景: 网页/图片缓存、临时数据缓存、避免重复计算的缓存(计算结果占用内存较大时)。
定义: 用来描述非必需的对象,强度比软引用更弱。
核心类: java.lang.ref.WeakReference
语法:
MyObject strongRef = new MyObject();
WeakReference weakRef = new WeakReference<>(strongRef);
strongRef = null; // 去掉强引用,只剩弱引用
// 尝试获取 (很可能马上为null)
MyObject retrieved = weakRef.get(); // 可能返回null
GC行为:
无论当前内存是否充足,只要发生垃圾回收(即使是Minor GC),并且对象仅被弱引用指向(没有强引用、软引用),那么这个对象就会被回收。
回收具有不确定性,随时可能发生。
特点:
生命周期极短: 一旦失去强引用,对象在下一次GC时几乎肯定被回收。
get()
方法: 同样可能返回null
。
防止内存泄漏的关键: 经典应用在规范映射(Canonicalizing Mappings) 和监听器/回调场景中,避免因持有对方引用而导致双方都无法被回收。WeakHashMap
是其典型代表(Key是弱引用)。
使用场景:
WeakHashMap
(Key弱引用):常用于实现类元信息缓存、监听器列表(防止监听器无法被回收导致内存泄漏)。
辅助性信息的关联(当主要对象被回收时,辅助信息也应自动释放)。
ThreadLocal
的内部实现ThreadLocalMap
的Entry
继承了WeakReference
(Key是弱引用指向ThreadLocal
对象),目的是防止ThreadLocal
对象本身因线程长时间存活(如线程池)而无法被回收。
定义: 也称为“幽灵引用”或“幻象引用”,是最弱的一种引用关系。
核心类: java.lang.ref.PhantomReference
语法:
ReferenceQueue queue = new ReferenceQueue<>();
MyResource resource = new MyResource(); // 可能持有Native资源
PhantomReference phantomRef = new PhantomReference<>(resource, queue);
resource = null; // 去掉强引用
// ... 稍后
// 无法通过 phantomRef.get() 获取对象,它永远返回 null!
// 监控队列
Reference extends MyResource> refFromQueue = queue.poll();
if (refFromQueue != null) {
// 对象已被回收,且进入了引用队列
// 在这里执行资源清理操作 (如关闭文件句柄、释放Native内存)
refFromQueue.clear(); // 彻底断开虚引用
}
GC行为:
完全不影响对象的生命周期。如果一个对象仅被虚引用指向,那么它和没有引用指向一样,GC会随时回收它。
关键区别: 虚引用必须和ReferenceQueue
联合使用。
特点:
get()
方法永远返回null
!不能通过虚引用来获取对象实例。
唯一作用: 在对象被GC回收后,GC会将其关联的虚引用对象放入引用队列。程序通过监控这个队列,可以精确知道对象何时被回收。
对象回收后的通知机制: 这是虚引用的核心价值。
使用场景:
精准的资源清理: 主要用于在对象被GC回收后,执行一些非常重要的、与Java对象本身无关的资源释放操作。典型例子是管理堆外内存(Direct ByteBuffer的Cleaner机制内部就使用了PhantomReference
)或文件句柄。finalize()
方法不可靠且已被废弃,虚引用+引用队列是更好的替代方案。
对象回收的监控/日志。
作用: 与软引用、弱引用、虚引用配合使用。当引用指向的对象被GC回收后,JVM会(在某个不确定的时间点)将引用对象本身(即SoftReference
/WeakReference
/PhantomReference
实例)放入这个队列。
核心类: java.lang.ref.ReferenceQueue
工作原理:
创建引用时关联一个ReferenceQueue
。
当引用指向的对象被GC回收后,JVM将这个引用对象(不是被回收的对象)放入队列。
程序通过轮询poll()
或阻塞remove()
方法从队列中取出引用对象。
取出的引用对象可以:
清理操作: (虚引用主要场景) 执行关联的清理逻辑(如释放Native资源)。
移除引用: 从一些管理容器中移除该引用,防止引用对象本身堆积造成内存浪费(例如WeakHashMap
会利用队列清理失效的Entry)。
重要性: 是实现“对象回收后动作”的关键桥梁。软/弱引用不一定要搭配队列,但虚引用必须搭配队列才有意义。
特性 | 强引用 | 软引用 | 弱引用 | 虚引用 |
---|---|---|---|---|
强度 | 最强 | 中 | 弱 | 最弱 (或无) |
GC影响 | 绝不回收 | 内存不足时回收 (OOM前) | 发现即回收 (下次GC) | 不影响回收 (随时可回收) |
get() |
返回对象 | 内存足返回对象;内存不足被回收则返回null |
未被回收返回对象;被回收则返回null |
永远返回 null |
队列 | 无 | 可选 | 可选 | 必须 |
主要用途 | 对象默认引用 | 内存敏感缓存 | 防止内存泄漏 (规范映射, 监听器清理) | 对象回收后通知与资源清理 (替代finalize) |
典型类 | 所有new 对象 |
SoftReference |
WeakReference , WeakHashMap (Key) |
PhantomReference (必须配ReferenceQueue ) |
回收时机 | 显式断链后 (可被回收) | 内存不足时 | 下次GC发生时 | 随时 (回收后入队通知) |
选择指南:
需要对象一直存在 -> 强引用 (注意及时置null
)
缓存对象,希望内存不足时自动释放 -> 软引用 (配合或不配合队列)
关联辅助数据/监听器,主要对象回收时自动解除关联 -> 弱引用 (常配合WeakHashMap
或队列)
需要精确知道对象被回收的时机并执行关键清理(尤其是非Java资源)-> 虚引用 (必须配合ReferenceQueue
)
软引用实现简单内存缓存:
public class ImageCache {
private final Map cache = new HashMap<>();
private final ReferenceQueue queue = new ReferenceQueue<>();
public void putImage(String key, BufferedImage image) {
// 清理队列中已被GC回收的软引用
cleanupQueue();
// 创建软引用并放入缓存,关联引用队列
SoftReference ref = new SoftReference<>(image, queue);
cache.put(key, ref);
}
public BufferedImage getImage(String key) {
cleanupQueue(); // 先清理
SoftReference ref = cache.get(key);
if (ref != null) {
BufferedImage image = ref.get();
if (image != null) {
return image; // 缓存命中
} else {
cache.remove(key); // 引用还在但对象已被回收,移除无效条目
}
}
return null; // 缓存未命中或失效
}
private void cleanupQueue() {
Reference extends BufferedImage> ref;
while ((ref = queue.poll()) != null) {
// 找到队列中的引用,并从缓存Map中移除对应的键(需要设计键与引用的关联)
// 通常需要额外设计数据结构(如WeakReference)来找到对应的key,这里简化处理
// 更常见的做法是使用WeakHashMap或Guava Cache等成熟库
cache.values().removeIf(value -> value == ref); // 效率不高,仅示意
}
}
}
弱引用防止内存泄漏 (WeakHashMap
示例):
public class ListenerManager {
private final Map listeners = new WeakHashMap<>();
public void addListener(EventListener listener, Object source) {
// Key (listener) 是弱引用。如果listener外部没有强引用了,它会被GC回收,Entry也会自动移除
listeners.put(listener, source);
}
// 当listener对象在其他地方没有强引用时,它会被GC回收,WeakHashMap会自动移除对应的Entry
// 无需显式调用removeListener,避免了因忘记注销导致的内存泄漏
}
虚引用管理堆外内存 (模拟 DirectByteBuffer
的 Cleaner
机制):
public class NativeResourceHolder {
private final long nativeHandle; // 假设代表Native资源指针
private final Cleaner cleaner;
public NativeResourceHolder() {
this.nativeHandle = allocateNativeResource(); // 分配Native资源
this.cleaner = Cleaner.create(this, new ResourceCleaner(nativeHandle));
}
// 内部静态类,执行实际的清理工作
private static class ResourceCleaner implements Runnable {
private final long handleToClean;
ResourceCleaner(long handle) {
this.handleToClean = handle;
}
@Override
public void run() {
// 确保在PhantomReference入队后被调用,释放Native资源
freeNativeResource(handleToClean);
System.out.println("Native resource freed for handle: " + handleToClean);
}
}
// Native方法(示意)
private native long allocateNativeResource();
private native void freeNativeResource(long handle);
}
// Cleaner内部简化原理 (JDK实际实现更复杂):
public class Cleaner {
private final PhantomReference
误用强引用: 最常见的泄漏原因(静态集合、未注销的监听器、缓存设计不当)。时刻警惕对象的生命周期。
软引用/弱引用缓存不检查get()
: 拿到null
后未正确处理,导致逻辑错误或NPE。使用前务必判空。
滥用软引用: 将所有缓存都用软引用,可能导致缓存命中率低(频繁被回收)或回收不及时(内存压力大时集中回收导致卡顿)。评估对象价值和内存占用。
弱引用导致过早回收: 如果弱引用的对象还在使用中(有强引用链),但某个关键的弱引用被回收了(例如WeakHashMap
的Key),可能导致意外行为。理解WeakHashMap
Key被回收的影响。
虚引用忘记关联队列或忘记处理队列: 虚引用失去意义。必须配合队列并主动轮询/处理。
引用对象本身的内存泄漏: 如果不断创建软/弱/虚引用对象(例如在缓存中),却不清理队列或管理容器,这些引用对象本身会堆积占用内存。及时清理引用队列中的失效引用。
优先使用成熟缓存库: 如Caffeine
, Guava Cache
等,它们内部精细地处理了引用类型、队列、并发和过期策略,比自己实现更健壮高效。
理解finalize()
的弊端: 它不可靠、性能差、可能导致对象复活,已被废弃。优先考虑虚引用+队列进行资源清理。