JVM(4)——引用类型
-
痛点引入: 为什么需要不同的引用类型?直接只用强引用不行吗?(内存泄漏风险、缓存管理粗粒度、对象生命周期监听需求)
-
核心作用: 解释引用类型如何让程序员与垃圾收集器(GC)协作,更精细地控制对象的生命周期,影响GC行为。
1. JVM垃圾回收(GC)基础回顾(简述)
-
可达性分析算法(GC Roots)是GC判断对象是否存活的基础。
-
对象从创建到被GC回收的生命周期(强可达 -> ... -> 不可达 -> 回收)。
-
核心概念: 引用类型直接影响对象在可达性分析链条中的“强度”,从而决定GC何时可以回收该对象。
2. 四种引用类型详解
2.1 强引用
-
定义: 最常见的引用类型,通过
new
关键字创建的对象默认就是强引用。 -
语法:
Object obj = new Object();
(obj
就是一个指向新创建Object
实例的强引用) -
特点:
-
最强引用: 只要强引用存在(即通过
obj
能访问到该对象),GC就绝对不会回收这个对象。 -
内存泄漏根源: 无意中保持的强引用(如静态集合长期持有对象、监听器未注销)是导致内存泄漏最常见的原因。
-
-
如何中断: 显式地将引用设置为
null
(obj = null;
),或者让引用超出作用域。之后对象变得可被GC回收(但非立即回收)。
2.2 软引用
-
定义: 用来描述一些还有用但并非必需的对象。
-
核心类:
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
(如果对象已被回收),使用前需检查。
-
-
使用场景: 网页/图片缓存、临时数据缓存、避免重复计算的缓存(计算结果占用内存较大时)。
2.3 弱引用
-
定义: 用来描述非必需的对象,强度比软引用更弱。
-
核心类:
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
对象本身因线程长时间存活(如线程池)而无法被回收。
-
2.4 虚引用
-
定义: 也称为“幽灵引用”或“幻象引用”,是最弱的一种引用关系。
-
核心类:
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()
方法不可靠且已被废弃,虚引用+引用队列是更好的替代方案。 -
对象回收的监控/日志。
-
3. 引用队列
-
作用: 与软引用、弱引用、虚引用配合使用。当引用指向的对象被GC回收后,JVM会(在某个不确定的时间点)将引用对象本身(即
SoftReference
/WeakReference
/PhantomReference
实例)放入这个队列。 -
核心类:
java.lang.ref.ReferenceQueue
-
工作原理:
-
创建引用时关联一个
ReferenceQueue
。 -
当引用指向的对象被GC回收后,JVM将这个引用对象(不是被回收的对象)放入队列。
-
程序通过轮询
poll()
或阻塞remove()
方法从队列中取出引用对象。 -
取出的引用对象可以:
-
清理操作: (虚引用主要场景) 执行关联的清理逻辑(如释放Native资源)。
-
移除引用: 从一些管理容器中移除该引用,防止引用对象本身堆积造成内存浪费(例如
WeakHashMap
会利用队列清理失效的Entry)。
-
-
-
重要性: 是实现“对象回收后动作”的关键桥梁。软/弱引用不一定要搭配队列,但虚引用必须搭配队列才有意义。
4. 对比总结与选择指南
特性 | 强引用 | 软引用 | 弱引用 | 虚引用 |
---|---|---|---|---|
强度 | 最强 | 中 | 弱 | 最弱 (或无) |
GC影响 | 绝不回收 | 内存不足时回收 (OOM前) | 发现即回收 (下次GC) | 不影响回收 (随时可回收) |
get() | 返回对象 | 内存足返回对象;内存不足被回收则返回null | 未被回收返回对象;被回收则返回null | 永远返回 null |
队列 | 无 | 可选 | 可选 | 必须 |
主要用途 | 对象默认引用 | 内存敏感缓存 | 防止内存泄漏 (规范映射, 监听器清理) | 对象回收后通知与资源清理 (替代finalize) |
典型类 | 所有new 对象 | SoftReference | WeakReference , WeakHashMap (Key) | PhantomReference (必须配ReferenceQueue ) |
回收时机 | 显式断链后 (可被回收) | 内存不足时 | 下次GC发生时 | 随时 (回收后入队通知) |
-
选择指南:
-
需要对象一直存在 -> 强引用 (注意及时置
null
) -
缓存对象,希望内存不足时自动释放 -> 软引用 (配合或不配合队列)
-
关联辅助数据/监听器,主要对象回收时自动解除关联 -> 弱引用 (常配合
WeakHashMap
或队列) -
需要精确知道对象被回收的时机并执行关键清理(尤其是非Java资源)-> 虚引用 (必须配合
ReferenceQueue
)
-
5. 实战应用与代码示例
-
软引用实现简单内存缓存:
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>)来找到对应的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;}@Overridepublic 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<Object> phantomRef;private final Runnable cleanupTask;private static final ReferenceQueue<Object> queue = new ReferenceQueue<>();private Cleaner(Object referent, Runnable task) {cleanupTask = task;phantomRef = new PhantomReference<>(referent, queue);// 通常会启动一个守护线程监控queue}public static Cleaner create(Object obj, Runnable task) {return new Cleaner(obj, task);}// 守护线程大致逻辑static {Thread cleanerThread = new Thread(() -> {while (true) {try {Reference<?> ref = queue.remove(); // 阻塞等待if (ref instanceof CleanerPhantomReference) {((CleanerPhantomReference) ref).clean();}} catch (InterruptedException e) { /* ... */ }}});cleanerThread.setDaemon(true);cleanerThread.start();}private static class CleanerPhantomReference extends PhantomReference<Object> {private final Runnable task;CleanerPhantomReference(Object referent, ReferenceQueue<? super Object> q, Runnable task) {super(referent, q);this.task = task;}void clean() {task.run();}} }
6. 常见陷阱与最佳实践
-
误用强引用: 最常见的泄漏原因(静态集合、未注销的监听器、缓存设计不当)。时刻警惕对象的生命周期。
-
软引用/弱引用缓存不检查
get()
: 拿到null
后未正确处理,导致逻辑错误或NPE。使用前务必判空。 -
滥用软引用: 将所有缓存都用软引用,可能导致缓存命中率低(频繁被回收)或回收不及时(内存压力大时集中回收导致卡顿)。评估对象价值和内存占用。
-
弱引用导致过早回收: 如果弱引用的对象还在使用中(有强引用链),但某个关键的弱引用被回收了(例如
WeakHashMap
的Key),可能导致意外行为。理解WeakHashMap
Key被回收的影响。 -
虚引用忘记关联队列或忘记处理队列: 虚引用失去意义。必须配合队列并主动轮询/处理。
-
引用对象本身的内存泄漏: 如果不断创建软/弱/虚引用对象(例如在缓存中),却不清理队列或管理容器,这些引用对象本身会堆积占用内存。及时清理引用队列中的失效引用。
-
优先使用成熟缓存库: 如
Caffeine
,Guava Cache
等,它们内部精细地处理了引用类型、队列、并发和过期策略,比自己实现更健壮高效。 -
理解
finalize()
的弊端: 它不可靠、性能差、可能导致对象复活,已被废弃。优先考虑虚引用+队列进行资源清理。