面试考点:软引用、弱引用、虚引用的使用场景和GC回收时机
阎王爷 👹 有本"生死簿" 📖,记录了每个人的寿命。
GC 也有本"引用簿",决定对象的生死!
今天咱们就来揭秘Java的四大引用类型!👀
🎭 引用类型全家福
引用类型家族:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
👑 强引用 (Strong Reference)
"铁饭碗" - 不死之身
💪 软引用 (Soft Reference)
"合同工" - 内存紧张就解雇
🌬️ 弱引用 (Weak Reference)
"临时工" - 下次GC就拜拜
👻 虚引用 (Phantom Reference)
"幽灵" - 随时可能消失,用于追踪
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
一图看懂引用强度 📊
强度: 强引用 > 软引用 > 弱引用 > 虚引用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
回收时机:
┌──────────────────────────────────┐
│ 强引用:永不回收(除非null) │ 🏆
├──────────────────────────────────┤
│ 软引用:内存不足时回收 │ 💪
├──────────────────────────────────┤
│ 弱引用:下次GC就回收 │ 🌬️
├──────────────────────────────────┤
│ 虚引用:随时回收(不影响生命周期) │ 👻
└──────────────────────────────────┘
👑 强引用(Strong Reference)
什么是强引用?
最普通的引用,你平时写的99%都是强引用!
Object obj = new Object(); // ← 这就是强引用!
String str = "Hello"; // ← 也是强引用!
List<String> list = new ArrayList<>(); // ← 还是强引用!
特点 ⚡
✅ 只要强引用存在,对象永不回收
✅ 宁可OOM,也不回收
✅ 最常用的引用方式
生活类比 🏠
强引用 = 房产证 🏡
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
只要你持有房产证:
- 房子就是你的
- 政府不能强拆
- 即使你欠债,也不能随便卖房
除非:
- 你主动卖掉(obj = null)
- 你去世了(对象不可达)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
内存泄漏示例 💧
public class MemoryLeak {
// ❌ 静态集合持有强引用
private static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 永远不会被GC!
}
}
问题:
- cache一直持有对象的强引用
- 即使不再使用,对象也不会被回收
- 最终导致OOM!💥
💪 软引用(Soft Reference)
什么是软引用?
"内存敏感"的缓存引用,内存充足时保留,内存不足时回收。
// 创建软引用
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
// 获取对象
byte[] data = softRef.get(); // 如果还在,返回对象;如果被回收了,返回null
回收时机 ⏰
内存充足:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC:我看到一个软引用对象...
GC:但是内存还很多,保留吧!
✅ 对象存活
内存紧张(快OOM了):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC:内存不够了,该清理软引用了!
GC:回收所有软引用对象!
💀 对象被回收
之后:
如果还不够 → 抛出OOM
如果够了 → 继续运行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
生活类比 📦
软引用 = 储物间的杂物 📦
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
房间宽敞:
- 杂物可以放着
- 不影响生活
房间拥挤:
- 扔掉杂物腾空间
- 只留必需品
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
经典用法:图片缓存 🖼️
public class ImageCache {
// 使用软引用缓存图片
private Map<String, SoftReference<Image>> cache = new HashMap<>();
public Image getImage(String path) {
// 1. 从缓存获取
SoftReference<Image> ref = cache.get(path);
if (ref != null) {
Image img = ref.get();
if (img != null) {
System.out.println("从缓存获取: " + path);
return img; // ✅ 缓存命中
}
}
// 2. 缓存未命中,加载图片
System.out.println("加载图片: " + path);
Image img = loadImage(path);
// 3. 放入缓存
cache.put(path, new SoftReference<>(img));
return img;
}
private Image loadImage(String path) {
// 从磁盘加载图片
return new Image(path);
}
}
优势:
- ✅ 内存充足时,缓存生效,性能好
- ✅ 内存不足时,自动清理,不会OOM
- ✅ 不需要手动管理缓存大小
软引用 + 引用队列 📮
// 引用队列:对象被回收时,软引用会进入这个队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
// 创建软引用时关联队列
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024], queue);
// 检查哪些软引用被回收了
Reference<? extends byte[]> ref = queue.poll();
if (ref != null) {
System.out.println("某个软引用对象被回收了!");
// 可以清理Map中的entry
}
应用:清理缓存Map中的无效Entry
public class SmartCache<K, V> {
private Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
private ReferenceQueue<V> queue = new ReferenceQueue<>();
public void put(K key, V value) {
// 清理已回收的entry
cleanUp();
// 添加新值
cache.put(key, new SoftReference<>(value, queue));
}
public V get(K key) {
cleanUp();
SoftReference<V> ref = cache.get(key);
return ref == null ? null : ref.get();
}
private void cleanUp() {
Reference<? extends V> ref;
while ((ref = queue.poll()) != null) {
// 移除已被回收的entry
cache.values().remove(ref);
}
}
}
🌬️ 弱引用(Weak Reference)
什么是弱引用?
"弱不禁风"的引用,只要GC一来,立刻被回收!
WeakReference<byte[]> weakRef = new WeakReference<>(new byte[1024]);
byte[] data = weakRef.get(); // 可能返回null(被GC了)
回收时机 ⏰
GC前:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
对象: 存在 ✅
弱引用.get(): 返回对象
GC执行中:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC: 发现一个只有弱引用的对象
GC: 不管内存够不够,直接回收!
💀 对象被回收
GC后:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
弱引用.get(): 返回null
生活类比 🍃
弱引用 = 一张便签纸 📝
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
清洁工(GC)一来:
- 便签纸立刻被扔掉
- 不管上面写了什么
- 不管是否有用
强引用 = 合同文件 📄
- 清洁工不敢扔
- 必须妥善保管
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
经典用法1:WeakHashMap 🗺️
// 普通HashMap
Map<Key, Value> map = new HashMap<>();
Key key = new Key();
map.put(key, value);
key = null; // ❌ value仍然被map持有,无法回收!
// WeakHashMap
Map<Key, Value> weakMap = new WeakHashMap<>();
Key key = new Key();
weakMap.put(key, value);
key = null; // ✅ 下次GC后,entry自动移除!
原理:
// WeakHashMap的Entry
static class Entry<K,V> extends WeakReference<K> {
V value;
Entry(K key, V value, ReferenceQueue<K> queue) {
super(key, queue); // key是弱引用!
this.value = value;
}
}
应用场景:
// 场景:为对象关联元数据,但不影响对象回收
public class ObjectMetadata {
private static WeakHashMap<Object, Metadata> metadataMap = new WeakHashMap<>();
public static void setMetadata(Object obj, Metadata meta) {
metadataMap.put(obj, meta);
}
public static Metadata getMetadata(Object obj) {
return metadataMap.get(obj);
}
}
// 使用
Object obj = new Object();
ObjectMetadata.setMetadata(obj, new Metadata("重要对象"));
obj = null; // ✅ 下次GC后,metadata自动清理!
经典用法2:ThreadLocal 🧵
// ThreadLocal内部使用WeakReference
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用!
value = v;
}
}
}
为什么用弱引用?
场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("data");
// 后来不用了
threadLocal = null;
如果是强引用:
❌ ThreadLocalMap仍持有threadLocal
❌ 造成内存泄漏
如果是弱引用:
✅ threadLocal被GC回收
✅ Entry的key变成null
✅ 下次get/set时清理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ 但value仍是强引用!
// 必须手动清理!
threadLocal.remove(); // ← 重要!
👻 虚引用(Phantom Reference)
什么是虚引用?
"形同虚设"的引用,最弱的引用,几乎没有实际用途...除了一个:对象回收追踪!
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
Object obj = phantomRef.get(); // ⚠️ 永远返回null!
特点 👀
✅ get()永远返回null
✅ 必须配合ReferenceQueue使用
✅ 对象回收前,虚引用会进入队列
✅ 用于追踪对象何时被回收
回收时机 ⏰
对象即将被回收:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GC: 准备回收对象
GC: 发现有虚引用
GC: 先把虚引用放入队列 ← 通知机制!
GC: 然后回收对象
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
应用可以:
监听队列 → 发现虚引用 → 得知对象已被回收
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
生活类比 💀
虚引用 = 讣告 📰
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
人去世后:
- 讣告会发布(虚引用进队列)
- 通知亲友(应用得知对象已死)
- 但讣告本身不能"复活"死者(get()返回null)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
应用:DirectByteBuffer清理 🧹
问题:DirectByteBuffer分配的是堆外内存,Java GC无法直接回收!
解决:使用虚引用 + Cleaner
public class DirectByteBuffer {
DirectByteBuffer(int cap) {
// 1. 分配堆外内存
long address = unsafe.allocateMemory(cap);
// 2. 创建Cleaner(基于虚引用)
Cleaner.create(this, new Deallocator(address));
}
private static class Deallocator implements Runnable {
private long address;
Deallocator(long address) {
this.address = address;
}
public void run() {
// 对象被回收时,释放堆外内存
unsafe.freeMemory(address);
}
}
}
流程:
1. DirectByteBuffer对象不可达
2. GC准备回收
3. Cleaner(虚引用)进入队列
4. ReferenceHandler线程检测到
5. 执行Deallocator.run()
6. 释放堆外内存 ✅
JDK 9+: Cleaner API
// JDK 9引入了新的Cleaner API
Cleaner cleaner = Cleaner.create();
class Resource {
private final Cleaner.Cleanable cleanable;
Resource() {
// 注册清理动作
this.cleanable = cleaner.register(this, new CleanAction());
}
static class CleanAction implements Runnable {
public void run() {
// 清理资源
System.out.println("资源被回收,执行清理!");
}
}
}
📊 四大引用对比表
| 引用类型 | 回收时机 | 使用场景 | 性能开销 | get()返回 |
|---|---|---|---|---|
| 强引用 | 永不回收 | 常规对象 | 无 | 对象 |
| 软引用 | 内存不足 | 缓存 | 低 | 对象/null |
| 弱引用 | 下次GC | 元数据、监听器 | 低 | 对象/null |
| 虚引用 | 随时 | 回收追踪 | 低 | 永远null |
🎯 实战案例
案例1:LRU缓存优化 💾
问题:传统LRU有固定容量限制
// 传统LRU:固定大小100
LRUCache<String, Image> cache = new LRUCache<>(100);
// 问题:
// - 内存充足时,只缓存100个(浪费)
// - 内存紧张时,仍缓存100个(可能OOM)
优化:结合软引用
public class SoftLRUCache<K, V> extends LinkedHashMap<K, SoftReference<V>> {
private final int maxSize;
public SoftLRUCache(int maxSize) {
super(16, 0.75f, true); // accessOrder=true
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, SoftReference<V>> eldest) {
return size() > maxSize;
}
public V get(Object key) {
SoftReference<V> ref = super.get(key);
return ref == null ? null : ref.get();
}
public V put(K key, V value) {
super.put(key, new SoftReference<>(value));
return value;
}
}
优势:
- ✅ 内存充足:可以缓存更多(软引用不回收)
- ✅ 内存紧张:自动清理(软引用被回收)
- ✅ 双重保护:LRU + 软引用
案例2:监听器泄漏防止 🎧
问题:监听器容易引起内存泄漏
// ❌ 问题代码
public class EventBus {
private List<Listener> listeners = new ArrayList<>();
public void register(Listener listener) {
listeners.add(listener); // 强引用!
}
// 如果忘记unregister,listener永远不会被回收!
}
解决:使用弱引用
// ✅ 优化代码
public class EventBus {
private List<WeakReference<Listener>> listeners = new ArrayList<>();
private ReferenceQueue<Listener> queue = new ReferenceQueue<>();
public void register(Listener listener) {
cleanup(); // 清理已回收的listener
listeners.add(new WeakReference<>(listener, queue));
}
public void fire(Event event) {
cleanup();
for (WeakReference<Listener> ref : listeners) {
Listener listener = ref.get();
if (listener != null) {
listener.onEvent(event);
}
}
}
private void cleanup() {
Reference<? extends Listener> ref;
while ((ref = queue.poll()) != null) {
listeners.remove(ref);
}
}
}
优势:
- ✅ 忘记unregister也不会泄漏
- ✅ listener不再使用时自动清理
💡 Pro Tips
Tip 1: 软引用不是万能缓存 ⚠️
// ❌ 错误:期望软引用能精确控制缓存大小
// 实际:软引用回收时机不确定!
// ✅ 正确:软引用 + 手动管理
public class HybridCache<K, V> {
private Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
private AtomicInteger size = new AtomicInteger(0);
private final int maxSize = 1000;
public void put(K key, V value) {
if (size.get() > maxSize) {
// 主动清理
cache.clear();
size.set(0);
}
cache.put(key, new SoftReference<>(value));
size.incrementAndGet();
}
}
Tip 2: ThreadLocal必须remove() 🧵
// ❌ 错误:依赖弱引用自动清理
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
threadLocal.set(new BigObject());
// 用完不管 → value仍然被强引用!
// ✅ 正确:手动remove
try {
threadLocal.set(new BigObject());
// 使用...
} finally {
threadLocal.remove(); // ← 必须!
}
Tip 3: 虚引用用于资源管理 🔧
// 管理本地资源(文件、网络连接等)
public class ResourceManager {
private Cleaner cleaner = Cleaner.create();
public class ManagedResource {
private final Cleaner.Cleanable cleanable;
ManagedResource(NativeResource resource) {
this.cleanable = cleaner.register(this, () -> {
resource.close(); // 自动关闭资源
});
}
}
}
🎓 面试要点
高频问题
Q1: 软引用和弱引用的区别?
答案:
| 维度 | 软引用 | 弱引用 |
|---|---|---|
| 回收时机 | 内存不足 | 每次GC |
| 适用场景 | 缓存 | 元数据、监听器 |
| 生存时间 | 较长 | 很短 |
Q2: WeakHashMap的key是弱引用,为什么value不是?
答案:
- key是弱引用:对象不再使用时,自动移除entry
- value是强引用:保证数据不丢失
- 如果value也是弱引用,数据随时可能丢失(无意义)
Q3: 虚引用有什么用?
答案: 主要用于追踪对象回收,典型应用:
- DirectByteBuffer的堆外内存清理
- 资源泄漏检测
- 对象回收时的后处理
🎉 总结
🎯 选择指南
需要什么? → 用哪种引用?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
常规对象 → 强引用 👑
内存敏感缓存 → 软引用 💪
临时关联数据 → 弱引用 🌬️
对象回收追踪 → 虚引用 👻
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 使用要点
- 强引用:99%的场景,注意避免泄漏
- 软引用:适合缓存,但要结合手动管理
- 弱引用:WeakHashMap、ThreadLocal的原理
- 虚引用:资源清理,配合Cleaner使用
记住:引用类型是GC的"指挥棒",掌握它们就能精准控制对象生命周期!🎯
🌟 "强软弱虚,各有千秋;选对引用,事半功倍!" 😎