引用类型
问题
Java 中有哪几种引用类型?它们的区别是什么?什么时候使用 WeakReference 和 SoftReference?
答案
四种引用类型
JDK 1.2 之后,Java 将引用分为四种强度,从强到弱依次为:
| 引用类型 | 类 | GC 回收条件 | 用途 |
|---|---|---|---|
| 强引用 | 直接赋值 | 永远不会回收(只要引用存在) | 普通对象引用 |
| 软引用 | SoftReference | 内存不足时回收 | 内存敏感缓存 |
| 弱引用 | WeakReference | 下次 GC 时必定回收 | 缓存、避免内存泄漏 |
| 虚引用 | PhantomReference | 随时可能回收,无法通过虚引用获取对象 | 跟踪对象被 GC 的时机 |
强引用(Strong Reference)
最常见的引用类型,通过 new 创建的对象默认就是强引用:
// 强引用:只要引用存在就不会被 GC
Object obj = new Object(); // obj 是强引用
// 即使 OOM 也不会回收强引用对象
// 只有显式置为 null 或超出作用域后才能被回收
obj = null; // 断开引用,对象变为可回收
特点:宁可 OOM 也不会回收强引用对象。
软引用(SoftReference)
被软引用关联的对象,在内存不足(即将 OOM)时会被回收:
import java.lang.ref.SoftReference;
// 创建软引用
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null; // 去掉强引用,只剩软引用
// 获取对象(内存充足时可以获取到)
Object retrieved = softRef.get();
if (retrieved != null) {
// 对象还在,可以使用
} else {
// 对象已被 GC 回收(内存不足时)
}
典型应用:内存敏感的缓存
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
/**
* 使用软引用实现的缓存
* 内存充足时缓存有效,内存不足时自动清理
*/
public class SoftCache<K, V> {
private final Map<K, SoftReference<V>> cache = new HashMap<>();
public void put(K key, V value) {
cache.put(key, new SoftReference<>(value));
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
if (ref != null) {
V value = ref.get();
if (value == null) {
// 软引用已被回收,清除 key
cache.remove(key);
}
return value;
}
return null;
}
}
Android 开发中常用 SoftReference 缓存 Bitmap。内存充足时直接使用缓存,内存紧张时 GC 自动回收。不过现代 Android 更推荐使用 LruCache 或 Glide 等库。
弱引用(WeakReference)
被弱引用关联的对象,只要发生 GC 就会被回收,不管内存是否充足:
import java.lang.ref.WeakReference;
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null; // 去掉强引用
// 此时只有弱引用指向对象
System.out.println(weakRef.get()); // 可能非 null(GC 还没执行)
System.gc(); // 建议 GC
// GC 后弱引用对象被回收
System.out.println(weakRef.get()); // null
典型应用 1:WeakHashMap
import java.util.WeakHashMap;
WeakHashMap<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "value");
System.out.println(map.size()); // 1
key = null; // key 失去强引用
System.gc();
// key 被回收后,entry 自动从 map 中移除
System.out.println(map.size()); // 0
典型应用 2:ThreadLocalMap
ThreadLocal 内部的 ThreadLocalMap 使用弱引用作为 key:
// ThreadLocalMap.Entry 源码(简化)
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用
value = v; // value 是强引用!
}
}
// key 被 GC 回收后变为 null,但 value 仍然存在 → 内存泄漏
// 所以必须调用 ThreadLocal.remove() 清理
虽然 ThreadLocalMap 的 key 是弱引用,但 value 是强引用。当 ThreadLocal 对象被回收后,key 变为 null,但 value 仍然被 Entry 强引用。在线程池场景下线程长期存活,这些 value 就泄漏了。用完 ThreadLocal 必须调用 remove()。
虚引用(PhantomReference)
最弱的引用,无法通过虚引用获取对象(get() 始终返回 null),主要用于跟踪对象被 GC 回收的时机:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
// 虚引用必须配合 ReferenceQueue 使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
System.out.println(phantomRef.get()); // 始终返回 null
obj = null;
System.gc();
// 对象被回收后,虚引用会被放入 ReferenceQueue
PhantomReference<?> ref = (PhantomReference<?>) queue.poll();
if (ref != null) {
System.out.println("对象已被回收,可以执行清理操作");
// 清理堆外资源
}
典型应用:DirectByteBuffer 的 Cleaner
// JDK 内部使用 Cleaner(基于虚引用)管理堆外内存
// DirectByteBuffer 创建时注册 Cleaner
// 当 DirectByteBuffer 被 GC 时,Cleaner 自动释放堆外内存
// JDK 9+ 提供了公开的 Cleaner API
import java.lang.ref.Cleaner;
public class CleanableResource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
// 清理逻辑(必须是静态类或 Lambda,不能引用外部对象)
private static class ResourceCleaner implements Runnable {
private long nativePtr;
ResourceCleaner(long ptr) { this.nativePtr = ptr; }
@Override
public void run() {
// 释放本地资源
freeNativeMemory(nativePtr);
}
}
public CleanableResource() {
long ptr = allocateNativeMemory();
this.cleanable = cleaner.register(this, new ResourceCleaner(ptr));
}
@Override
public void close() {
cleanable.clean(); // 手动触发清理
}
}
ReferenceQueue
引用队列用于跟踪引用对象被 GC 的时机:
import java.lang.ref.*;
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 创建软/弱引用时关联 ReferenceQueue
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj, queue);
obj = null;
System.gc();
// 对象被回收后,引用对象被放入队列
Reference<?> ref = queue.poll();
if (ref == weakRef) {
System.out.println("weakRef 指向的对象已被回收");
// 可以在这里做清理工作
}
| 引用类型 | ReferenceQueue 行为 |
|---|---|
| 软引用 | 内存不足被回收时入队 |
| 弱引用 | GC 回收时入队 |
| 虚引用 | 必须配合 ReferenceQueue 使用,GC 回收时入队 |
引用类型对比摘要
| 特性 | 强引用 | 软引用 | 弱引用 | 虚引用 |
|---|---|---|---|---|
| 创建方式 | new Object() | new SoftReference<>(obj) | new WeakReference<>(obj) | new PhantomReference<>(obj, queue) |
get() 返回值 | 对象本身 | 对象或 null | 对象或 null | 始终 null |
| GC 回收条件 | 不可达时 | 内存不足时 | 下次 GC 时 | 随时 |
| 典型用途 | 日常编码 | 内存敏感缓存 | WeakHashMap、ThreadLocal | Cleaner、堆外内存回收 |
常见面试问题
Q1: 四种引用类型的区别是什么?
答案:
从强到弱:强引用 → 软引用 → 弱引用 → 虚引用
- 强引用:只要引用存在就不会被 GC,宁可 OOM 也不回收
- 软引用:内存不足时才回收,适合做缓存
- 弱引用:下次 GC 必定回收,适合做非必需的对象引用(如 WeakHashMap)
- 虚引用:无法获取对象,只能通过 ReferenceQueue 感知对象被回收,用于清理资源
Q2: WeakReference 和 SoftReference 的使用场景有什么区别?
答案:
| 引用 | 回收时机 | 适用场景 |
|---|---|---|
| SoftReference | 内存不足时 | 缓存(图片缓存、热点数据缓存),希望尽可能保留但内存紧张时可以丢弃 |
| WeakReference | 下次 GC 时 | 避免内存泄漏的辅助引用(WeakHashMap、监听器引用),引用关系不影响对象的生命周期 |
简单来说:SoftReference 是"能缓存就缓存",WeakReference 是"不阻止回收"。
Q3: ThreadLocal 为什么用弱引用?还是会内存泄漏吗?
答案:
ThreadLocalMap 的 Entry 的 key 使用弱引用指向 ThreadLocal 对象:当 ThreadLocal 对象没有外部强引用时,GC 会回收 ThreadLocal,Entry 的 key 变为 null。
但 value 是强引用,key 为 null 后 value 无法被访问也无法被回收。在线程池场景下线程长期存活,这些"key 为 null 的 entry"就是泄漏。
虽然 ThreadLocalMap 在 get()/set()/remove() 时会清理 key 为 null 的 entry,但如果之后不再调用这些方法,泄漏就无法被清理。
结论:弱引用减轻了泄漏问题但没有完全解决,必须在 finally 中调用 remove()。
Q4: 虚引用有什么实际用途?
答案:
虚引用的 get() 始终返回 null,它的唯一用途是追踪对象被 GC 回收的时机,配合 ReferenceQueue 使用。
实际应用:
- DirectByteBuffer 的堆外内存回收:JDK 内部使用 Cleaner(基于虚引用),当 DirectByteBuffer 被 GC 时通过 Cleaner 释放堆外内存
- JDK 9+ Cleaner API:替代
finalize(),提供更可靠的资源清理机制 - NIO 框架(如 Netty)使用虚引用检测 ByteBuf 泄漏
Q5: ReferenceQueue 的作用是什么?
答案:
ReferenceQueue 是一个队列,当软引用/弱引用/虚引用指向的对象被 GC 回收时,引用对象本身会被放入关联的 ReferenceQueue 中。
通过轮询 ReferenceQueue,可以知道哪些对象被回收了,从而做相应的清理工作(如清除缓存 Map 中的 key、释放堆外资源等)。
虚引用必须配合 ReferenceQueue 使用,软引用和弱引用可选。
Q6: 如何实现一个内存敏感的缓存?
答案:
使用 SoftReference + ReferenceQueue:
public class MemoryCache<K, V> {
private final Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
public void put(K key, V value) {
// 先清理已被回收的引用
cleanUp();
cache.put(key, new KeyedSoftReference<>(key, value, queue));
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
return ref != null ? ref.get() : null;
}
private void cleanUp() {
Reference<? extends V> ref;
while ((ref = queue.poll()) != null) {
cache.remove(((KeyedSoftReference<K, V>) ref).key);
}
}
// 扩展 SoftReference,记住 key 方便清理
static class KeyedSoftReference<K, V> extends SoftReference<V> {
final K key;
KeyedSoftReference(K key, V value, ReferenceQueue<V> queue) {
super(value, queue);
this.key = key;
}
}
}
不过实际项目中更推荐使用 Caffeine 缓存库,它内置了 softValues() 和 weakKeys() 支持。