👻 Java四大引用类型:对象的"生死簿"!

55 阅读9分钟

面试考点:软引用、弱引用、虚引用的使用场景和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: 虚引用有什么用?

答案: 主要用于追踪对象回收,典型应用:

  1. DirectByteBuffer的堆外内存清理
  2. 资源泄漏检测
  3. 对象回收时的后处理

🎉 总结

🎯 选择指南

需要什么? → 用哪种引用?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
常规对象          → 强引用 👑
内存敏感缓存      → 软引用 💪
临时关联数据      → 弱引用 🌬️
对象回收追踪      → 虚引用 👻
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📋 使用要点

  1. 强引用:99%的场景,注意避免泄漏
  2. 软引用:适合缓存,但要结合手动管理
  3. 弱引用:WeakHashMap、ThreadLocal的原理
  4. 虚引用:资源清理,配合Cleaner使用

记住:引用类型是GC的"指挥棒",掌握它们就能精准控制对象生命周期!🎯

🌟 "强软弱虚,各有千秋;选对引用,事半功倍!" 😎