一张图搞懂 Java 四种引用——强软弱虚的原理与实战场景

一、从一道面试题说起

先来看这段代码——它在一线大厂的面试中出镜率极高:

public class ReferenceDemo {
    public static void main(String[] args) {
        Object obj = new Object();                          // ①
        WeakReference<Object> weak = new WeakReference<>(obj); // ②
        obj = null;                                         // ③
        System.gc();                                        // ④
        System.out.println(weak.get());                     // ⑤ 输出 null
    }
}

运行结果:null

为什么 obj 被置空后,WeakReference 就再也拿不到那个对象了?这一切都要从 Java 的四种引用说起。

WeakReference 的生命周期可以用下面的流程图来理解:

flowchart LR
    A["new Object()
堆上对象"
]:::object B["obj(强引用)"]:::strong C["WeakReference(弱引用)"]:::weak A -->|"② 弱引用指向"| C A -->|"① 强引用指向"| B B -.->|"③ obj = null
强引用断开"
| X1[" "] A -.->|"④ GC 触发"| X2["回收对象"]:::gc classDef object fill:#f9f,stroke:#333,color:#000 classDef strong fill:#ff6b6b,stroke:#c0392b,color:#000 classDef weak fill:#ffd93d,stroke:#f39c12,color:#000 classDef gc fill:#6bcb77,stroke:#27ae60,color:#000

二、强引用——最常见的引用

我们在日常写代码时,99% 的情况用的都是强引用:

User user = new User();     // 强引用
List<String> list = new ArrayList<>();  // 强引用

回收规则: 只要对象存在强引用指向它,GC 永远不会回收它,即使抛出 OOM 也不会。

这意味着下面这段代码就是一颗定时炸弹:

public static final List<Data> CACHE = new ArrayList<>();
while (true) {
    CACHE.add(new Data());  // 强引用链:CACHE → ArrayList → Data
    // 内存只增不减,直到 OOM
}

经典应用场景: 全局缓存、静态集合、Spring 的 Bean 容器——这些都是强引用在背后支撑。

三、软引用——内存敏感缓存的首选

软引用告诉 GC:”这个对象你看着办,内存够就先留着,不够就收掉。”

// 软引用的典型用法
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);

// 获取对象
Bitmap cachedBitmap = softRef.get();
if (cachedBitmap == null) {
    // 被 GC 回收了,重新加载
    cachedBitmap = loadBitmap();
}

回收规则:
– 内存充足 → 不回收
– 内存紧张,即将 OOM → 回收
注意: 不是触发 GC 就回收,而是触发 GC 后内存仍然不够时才回收

软引用的决策逻辑非常清晰:

flowchart TD
    Start["软引用指向的对象"] --> Check{"GC 触发"}
    Check -->|"当前堆内存充足"| Keep["保留对象
softRef.get() 返回正常"
] Check -->|"当前堆内存不足"| Recycle["回收对象
softRef.get() 返回 null"
] Keep --> End["继续使用缓存"] Recycle --> Oom["腾出空间,避免 OOM"] style Keep fill:#6bcb77,stroke:#27ae60,color:#000 style Recycle fill:#ff6b6b,stroke:#c0392b,color:#000

实战:图片缓存系统

public class ImageCache {
    private final Map<String, SoftReference<BufferedImage>> cache = new HashMap<>();

    public BufferedImage get(String key) {
        SoftReference<BufferedImage> ref = cache.get(key);
        if (ref != null) {
            BufferedImage img = ref.get();
            if (img != null) return img;
        }
        // 缓存失效,从磁盘加载
        BufferedImage img = loadFromDisk(key);
        cache.put(key, new SoftReference<>(img));
        return img;
    }
}

为什么这里用软引用而不是 WeakHashMap? 因为图片是我们想尽可能保留的,只有在 JVM 内存告急时才释放——恰好是软引用的设计目标。

四、弱引用——用完即弃

弱引用比软引用 “更软”:只要发生 GC,弱引用指向的对象就会被回收,不管内存够不够。

WeakReference<Object> weak = new WeakReference<>(new Object());
System.gc();
System.out.println(weak.get());  // null,一次 GC 就被回收了

4.1 WeakHashMap

WeakHashMap 是弱引用最经典的应用——它的 Entry 继承自 WeakReference:

Map<Key, Value> map = new WeakHashMap<>();
Key key = new Key("session-1");
map.put(key, "data");
System.out.println(map.size());  // 1

key = null;  // 唯一的强引用被切断
System.gc();
System.out.println(map.size());  // 0,自动清理了

核心价值: 当 key 的外部强引用消失后,WeakHashMap 自动删除对应的 Entry,无需手动 remove()。

4.2 ThreadLocal 的内存泄漏之谜

ThreadLocal 是弱引用的另一个重要应用者。看它的内部结构:

// ThreadLocalMap.Entry 的简化源码
static class Entry extends WeakReference<ThreadLocal> {
    Object value;
    Entry(ThreadLocal k, Object v) {
        super(k);  // key 是弱引用!
        value = v; // value 是强引用!
    }
}

这就解释了经典的内存泄漏场景:

ThreadLocal<Session> tl = new ThreadLocal<>();
tl.set(new Session("admin"));

// 如果忘记 remove()
// tl = null;  // ThreadLocal 对象变成弱引用可达,下一轮 GC 被回收
// 但 Entry 中的 value(Session 对象)仍然被强引用着!
// 只要线程存活,value 就永远无法被回收 → 内存泄漏

ThreadLocal 内存泄漏的本质一目了然:

flowchart TB
    subgraph "Thread"
        T["Thread 对象"] --> Map["ThreadLocalMap"]
        Map --> E1["Entry 1
key: WeakReference
value: Session 对象"
] Map --> E2["Entry 2
..."
] end subgraph "外部" TL["ThreadLocal tl"]:::strong end TL -- "set()" --> E1 TL -.->|"tl = null 后
弱引用断裂"
| Break["Key 被 GC 回收"] E1 -->|"value 仍是强引用
Session 对象无法回收!"
| Leak["☠ 内存泄漏"]:::leak classDef strong fill:#ff6b6b,stroke:#c0392b,color:#000 classDef leak fill:#e74c3c,stroke:#c0392b,color:#fff

解决方案:

// 务必在 finally 块中清理
ThreadLocal<Session> tl = new ThreadLocal<>();
try {
    tl.set(new Session("admin"));
    // ... 业务逻辑
} finally {
    tl.remove();  // 清除 Entry,避免内存泄漏
}

五、虚引用——最弱的引用

虚引用是四种引用中最弱的——你根本通过它拿不到对象

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);

System.out.println(phantom.get());  // 永远返回 null!

那它有什么用? 当虚引用指向的对象被回收时,JVM 会将这个虚引用入队到 ReferenceQueue 中。这提供了一种 “对象被回收后通知我” 的机制。

经典应用:NIO DirectByteBuffer 的回收

// DirectByteBuffer 内部核心逻辑(简化版)
public class DirectByteBuffer {
    private static class Deallocator implements Runnable {
        private final long address;  // 堆外内存地址

        @Override
        public void run() {
            // unsafe.freeMemory(address);  // 释放堆外内存
        }
    }

    private final Cleaner cleaner;  // 虚引用的具体实现

    DirectByteBuffer(int cap) {
        // 分配堆外内存
        long address = unsafe.allocateMemory(cap);
        cleaner = Cleaner.create(this, new Deallocator(address));
        // 当 DirectByteBuffer 对象被回收时,
        // Cleaner 会触发 Deallocator 释放堆外内存
    }
}

为什么需要这个机制?因为 DirectByteBuffer 本身是堆上的一个小对象(几十字节),但它维护的堆外内存可能是几十 MB。如果只靠 Finalizer 来释放堆外内存,GC 压力巨大,而且 Finalizer 执行时机不确定。通过虚引用 + ReferenceQueue,JVM 可以在对象被回收的同时触发清理动作,既及时又高效。

flowchart LR
    subgraph "堆内存"
        DBB["DirectByteBuffer 对象
堆上几十字节"
]:::heap end subgraph "堆外内存" Off["堆外内存区域
可能几十 MB"
]:::offheap end DBB -->|"维护引用"| Off DBB -->|"绑定了虚引用"| Cleaner["Cleaner
PhantomReference 实现"
] DBB -.->|"DBB 被 GC 回收后"| Cleaner Cleaner -->|"自动触发"| Deallocator["Deallocator.run()
释放堆外内存"
] classDef heap fill:#f9f,stroke:#333,color:#000 classDef offheap fill:#ffd93d,stroke:#f39c12,color:#000

六、四种引用一图对比

特性 强引用 软引用 弱引用 虚引用
回收时机 永不回收(OOM 也不回收) 内存不足时回收 下一次 GC 就回收 任何时候
get() 返回值 对象本身 对象(可能为 null) 对象(可能为 null) 永远为 null
典型用途 普通对象引用 内存敏感缓存 WeakHashMap、ThreadLocal DirectByteBuffer 堆外内存释放
配合 ReferenceQueue 不适用 回收前入队 回收前入队 回收后入队

回收时机速记口诀:

强永不、软不够、弱下次、虚是 null

七、实战:自己实现一个”可自动清理的缓存”

有了前面的基础,我们可以手写一个既安全又高效的缓存:

public class AutoCleanCache<K, V> {
    private final ConcurrentHashMap<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
    private final ReferenceQueue<V> queue = new ReferenceQueue<>();

    public void put(K key, V value) {
        expungeStaleEntries();  // 清理已被回收的条目
        cache.put(key, new SoftReference<>(value, queue));
    }

    public V get(K key) {
        SoftReference<V> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }

    private void expungeStaleEntries() {
        Reference extends V> ref;
        while ((ref = queue.poll()) != null) {
            // 从 cache 中移除对应的 key
            cache.entrySet().removeIf(entry -> entry.getValue() == ref);
        }
    }
}

工作原理:
1. 使用 SoftReference 包装 value,内存紧张时自动回收
2. ReferenceQueue 监控哪些 value 被回收了
3. 每次 put 时顺带清理一次,不引入额外线程

总结

  1. 强引用:默认引用类型,GC 永不回收,OOM 风险需自行管理
  2. 软引用:内存敏感缓存的利器,内存充足时保留,不足时释放
  3. 弱引用:一次 GC 就回收,WeakHashMap 和 ThreadLocal 是其代表,但 ThreadLocal 用完后务必 remove()
  4. 虚引用:get() 永远 null,用于精准监听对象回收事件,NIO 堆外内存依赖它

四种引用是理解 JVM 内存回收机制的钥匙。掌握了它们,你不仅能写出更好的代码,面试时遇到”你对 Java 引用了解多少”这类问题,也能从源码到实战讲得清清楚楚。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容