跳到主要内容

引用类型

问题

Java 中有哪几种引用类型?它们的区别是什么?什么时候使用 WeakReference 和 SoftReference?

答案

四种引用类型

JDK 1.2 之后,Java 将引用分为四种强度,从强到弱依次为:

引用类型GC 回收条件用途
强引用直接赋值永远不会回收(只要引用存在)普通对象引用
软引用SoftReference内存不足时回收内存敏感缓存
弱引用WeakReference下次 GC 时必定回收缓存、避免内存泄漏
虚引用PhantomReference随时可能回收,无法通过虚引用获取对象跟踪对象被 GC 的时机

强引用(Strong Reference)

最常见的引用类型,通过 new 创建的对象默认就是强引用:

StrongReference.java
// 强引用:只要引用存在就不会被 GC
Object obj = new Object(); // obj 是强引用

// 即使 OOM 也不会回收强引用对象
// 只有显式置为 null 或超出作用域后才能被回收
obj = null; // 断开引用,对象变为可回收

特点:宁可 OOM 也不会回收强引用对象。

软引用(SoftReference)

被软引用关联的对象,在内存不足(即将 OOM)时会被回收:

SoftReferenceExample.java
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 回收(内存不足时)
}

典型应用:内存敏感的缓存

SoftReferenceCache.java
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 就会被回收,不管内存是否充足:

WeakReferenceExample.java
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

WeakHashMapExample.java
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:

ThreadLocalWeakRef.java
// 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() 清理
ThreadLocal 内存泄漏

虽然 ThreadLocalMap 的 key 是弱引用,但 value 是强引用。当 ThreadLocal 对象被回收后,key 变为 null,但 value 仍然被 Entry 强引用。在线程池场景下线程长期存活,这些 value 就泄漏了。用完 ThreadLocal 必须调用 remove()

虚引用(PhantomReference)

最弱的引用,无法通过虚引用获取对象get() 始终返回 null),主要用于跟踪对象被 GC 回收的时机:

PhantomReferenceExample.java
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

CleanerExample.java
// 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 的时机:

ReferenceQueueExample.java
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、ThreadLocalCleaner、堆外内存回收

常见面试问题

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 使用。

实际应用:

  1. DirectByteBuffer 的堆外内存回收:JDK 内部使用 Cleaner(基于虚引用),当 DirectByteBuffer 被 GC 时通过 Cleaner 释放堆外内存
  2. JDK 9+ Cleaner API:替代 finalize(),提供更可靠的资源清理机制
  3. 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() 支持。

相关链接