Lesson 50 · Java 高级特性

四种引用类型:强引用、软引用、弱引用、虚引用

中级·#Java·#GC·#引用

第 1 站

强引用:Java 中最常见的引用,也是 OOM 的元凶

强引用(Strong Reference)是 Java 中最基本的引用类型——你写的几乎每一行代码中的引用都是强引用。它的特性只有一个:只要强引用存在,GC 绝不回收对象

StrongRefDemo.java · 强引用的行为
public class StrongRefDemo {
    public static void main(String[] args) {
        Object obj = new Object();   // obj 是强引用

        // 即使内存不够,GC 也不会回收 obj 指向的对象
        // 宁可抛出 OOM,也不会回收强引用对象

        obj = null;  // ★ 显式设为 null 才能断开引用
        // 此时对象变为不可达,可以被 GC 回收
    }
}

// 常见的强引用泄漏场景:
// ① 集合中对象忘记 remove
List<Object> cache = new ArrayList<>();
cache.add(bigObject);    // cache 强引用 bigObject → 永不回收

// ② 静态集合
static Map<String, Object> globalMap = new HashMap<>();
globalMap.put("key", bigObject);  // 静态变量生命周期=JVM 生命周期

// ③ ThreadLocal 中的 Value(见 ThreadLocal 那篇)
GC 判断标准:
从 GC Roots 出发,通过强引用链能到达的对象 = 存活对象。
只有当一个对象没有任何强引用指向它时,才可能被 GC 回收。
GC Roots 可达性分析 GC Roots 栈帧局部变量 静态变量 常量引用 JNI 引用 User 对象 Address 对象 孤立对象 A 孤立对象 B 可达性分析 绿色 = 可达(存活) 红色虚线 = 不可达 不可达对象在 下次 GC 时被回收 强引用链断裂 = 对象死亡 核心结论:强引用 = 生命线。只要有一条强引用链从 GC Root 到对象,对象就不会被回收。 软引用、弱引用、虚引用的本质区别在于:它们对 GC 可达性的影响程度不同。
图 1GC Roots 可达性分析:从 GC Roots 出发,沿强引用链可达的对象是存活的;不可达对象即使互相引用也会被回收
第 2 站

软引用 SoftReference:内存敏感的缓存利器

软引用(Soft Reference)的特性:内存充足时不回收,内存不足时回收。这个特性使它非常适合用来实现内存敏感的缓存。

SoftRefCache.java · 软引用实现图片缓存
public class ImageCache {
    // key=图片路径, value=图片对象的软引用
    private Map<String, SoftReference<Image>> cache = new HashMap<>();

    public Image getImage(String path) {
        SoftReference<Image> ref = cache.get(path);
        if (ref != null) {
            Image img = ref.get();  // ★ 可能返回 null(已被 GC 回收)
            if (img != null) {
                return img;  // 缓存命中
            }
        }
        // 缓存未命中或已被回收,重新加载
        Image img = loadImageFromDisk(path);
        cache.put(path, new SoftReference<>(img));
        return img;
    }
}

/*
 * GC 行为:
 * - 内存充足 → SoftReference 指向的对象不会被回收
 * - 内存紧张 → GC 会回收 SoftReference 指向的对象(在抛出 OOM 之前)
 *
 * JDK 回收策略(近似):
 * 软引用对象的存活时间 < (JVM 空闲内存 MB)
 * 即空闲内存越多,软引用存活越久
 */
软引用什么时候被回收?精确规则是什么?

JVM 规范只说"在即将发生 OOM 之前回收软引用"。具体实现因 GC 收集器不同:

-XX:SoftRefLRUPolicyMSPerMB(默认 1000ms)控制软引用的存活策略:

如果堆的空闲内存是 10MB,则软引用对象最多存活 10 × 1000 = 10 秒。超过这个时间就会在下一次 GC 时被回收。空闲内存越小,回收越积极。

SoftReference 做缓存真的可靠吗?有什么坑?

坑 1:GC 回收时机不可控。内存充足时可能一直不回收(浪费内存),内存紧张时突然全部回收(缓存击穿)。

坑 2:SoftReference 对象本身需要内存开销(每个约 40 字节)。

坑 3:现代生产环境更推荐 LRU Cache(如 Guava Cache、Caffeine),淘汰策略可控、可观测。

结论:SoftReference 适合做"锦上添花"的二级缓存,不适合作为核心缓存方案。
第 3 站

弱引用 WeakReference:下次 GC 必定回收

弱引用(Weak Reference)的特性:无论内存是否充足,只要发生 GC,弱引用指向的对象就会被回收。这使得弱引用适合做"不希望阻止对象被回收"的关联映射。

WeakRefDemo.java · 弱引用行为演示
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);

System.out.println(weakRef.get());  // java.lang.Object@xxx

obj = null;   // 断开强引用
System.gc();   // 建议 GC

System.out.println(weakRef.get());  // null ★ 对象已被回收!

// 对比软引用:
Object obj2 = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj2);
obj2 = null;
System.gc();
System.out.println(softRef.get());  // 仍然有值(内存充足不回收)

弱引用最经典的应用是 WeakHashMapThreadLocal 的 Entry

WeakHashMapDemo.java · WeakHashMap 自动清理
WeakHashMap<Object, String> map = new WeakHashMap<>();

Object key1 = new Object();
Object key2 = new Object();

map.put(key1, "value1");
map.put(key2, "value2");

System.out.println(map.size());  // 2

key1 = null;  // 断开 key1 的强引用
System.gc();

// ★ key1 对应的 Entry 被自动移除!
System.out.println(map.size());  // 1

/*
 * WeakHashMap 原理:
 * - Entry 继承 WeakReference,key 是弱引用
 * - 当 key 不再被外部强引用时,GC 回收 key
 * - WeakHashMap 在下次操作时,通过 ReferenceQueue 感知到 key 被回收
 * - 自动移除对应的 Entry(包括 value)
 */
ThreadLocal 的 Entry 为什么用弱引用?

ThreadLocalMap 的 Entry 继承了 WeakReference,其 key(ThreadLocal 实例)是弱引用。这样当外部不再持有 ThreadLocal 变量时,ThreadLocal 实例本身可以被 GC 回收。

但这只解决了"ThreadLocal 对象泄漏"的一半问题——Entry 中的 value 是强引用,如果线程不销毁,value 仍然泄漏。这就是 ThreadLocal 内存泄漏的核心矛盾(详见 ThreadLocal 专题)。

第 4 站

虚引用 PhantomReference:对象死亡前的最后通知

虚引用是四种引用中最特殊的一种:它不影响对象的生命周期,也无法通过 get() 获取对象。它的唯一作用是在对象被 GC 回收时,将通知发送到关联的 ReferenceQueue。

PhantomRefDemo.java · 虚引用的行为
ReferenceQueue<Object> queue = new ReferenceQueue<>();

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

// ★ get() 永远返回 null!虚引用无法获取对象
System.out.println(phantom.get());  // null

obj = null;  // 断开强引用
System.gc();

// GC 回收对象后,PhantomReference 被放入队列
Reference<?> ref = queue.poll();
System.out.println(ref == phantom);  // true ★ 确认对象已被回收

/*
 * 虚引用的设计意图:
 * ① 无法通过虚引用获取对象 → 不会"意外复活"对象
 * ② 对象被回收时收到通知 → 可以做清理工作(释放堆外内存等)
 * ③ 比 finalize() 更安全 → 不会导致对象复活或性能问题
 */

虚引用最典型的真实应用是 JDK 9+ 的 Cleaner 机制和 DirectByteBuffer 的清理。

特性finalize()PhantomReference + Cleaner
复活对象可以在 finalize() 中重新建立强引用(危险!)不可能——get() 永远返回 null
执行线程Finalizer 线程(单线程,可能阻塞)Cleaner 专用线程池
异常处理异常被吞掉,后续 finalize 不再执行异常不影响其他清理任务
性能影响对象至少多存活一个 GC 周期无额外 GC 开销
JDK 态度JDK 9 起标记为 @Deprecated官方推荐的替代方案
为什么 finalize() 被废弃?

finalize() 有三个致命问题:

1. 对象复活:在 finalize() 中可以 SomeClass.instance = this; 将自身赋给静态变量,对象重新可达——但 finalize() 只会调用一次,第二次 GC 直接回收,不做清理。

2. 性能灾难:需要 finalize 的对象会被放入 Finalizer 队列,由单个 Finalizer 线程依次执行。如果 finalize() 执行慢,整个回收流程被阻塞,对象堆积导致 OOM。

3. 不确定性:JVM 不保证 finalize() 何时被调用,甚至不保证会被调用(程序退出时可能跳过)。依赖 finalize() 释放资源是极其危险的。

CleanerDemo.java · Cleaner 替代 finalize()
import java.lang.ref.Cleaner;

public class ResourceHolder implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // ★ 清理逻辑必须放在独立的静态内部类中
    // 不能持有外部类的引用(否则阻止回收)
    private static class CleanAction implements Runnable {
        private final long nativeHandle;

        CleanAction(long handle) { this.nativeHandle = handle; }

        @Override
        public void run() {
            // ★ 对象被 GC 后,Cleaner 会调用此方法
            freeNativeResource(nativeHandle);
        }
    }

    private final Cleaner.Cleanable cleanable;

    public ResourceHolder(long handle) {
        // 注册清理动作:当 this 被 GC 时,执行 CleanAction
        this.cleanable = cleaner.register(this, new CleanAction(handle));
    }

    @Override
    public void close() {
        cleanable.clean();  // 主动清理(不必等 GC)
    }
}

/*
 * Cleaner 底层使用 PhantomReference + ReferenceQueue
 * JDK 内部 DirectByteBuffer 的 Cleaner 就是这样实现的:
 * 当 DirectByteBuffer 被 GC 时,Cleaner 负责释放堆外内存
 */
第 5 站

ReferenceQueue:引用回收的通知机制

ReferenceQueue 是软引用、弱引用、虚引用的"回收通知队列"。当引用指向的对象被 GC 回收后,引用本身会被加入队列,应用程序可以从队列中获取通知并做清理。

ReferenceQueueDemo.java · ReferenceQueue 使用模式
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Map<WeakReference<Object>, String> metadata = new HashMap<>();

// 创建对象并注册弱引用
for (int i = 0; i < 5; i++) {
    Object obj = new Object();
    WeakReference<Object> ref = new WeakReference<>(obj, queue);
    metadata.put(ref, "metadata-" + i);
    // obj 局部变量,循环结束后无强引用
}

System.gc();

// ★ 从队列中取出被回收的引用,做清理
Reference<? extends Object> ref;
while ((ref = queue.poll()) != null) {
    String meta = metadata.remove(ref);
    System.out.println("清理元数据: " + meta);
}

/*
 * 各引用类型的入队时机:
 * 软引用:对象被 GC 回收后入队
 * 弱引用:对象被 GC 回收后入队
 * 虚引用:★ 对象被 GC 回收后,从 Finalizer 队列移除后才入队
 *         (即对象真的"死透了"才入队,不像 finalize 可能复活对象)
 */
引用类型get() 返回值必须配合 Queue?入队时机
强引用N/A(不适用)N/AN/A
软引用存活对象 或 null可选对象被回收后
弱引用存活对象 或 null可选对象被回收后
虚引用永远返回 null必须对象 finalize 后入队
第 6 站

总结:四种引用全景对比与使用建议

引用类型GC 回收条件典型用途get() 返回值生产案例
强引用永不回收(只要引用存在)一切正常业务对象N/A所有 Java 代码
软引用内存不足时回收内存敏感的缓存对象 / nullAndroid BitmapCache
弱引用下次 GC 必定回收关联映射、防泄漏对象 / nullWeakHashMap、ThreadLocal
虚引用随时回收(等同无引用)堆外内存清理永远 nullDirectByteBuffer Cleaner
面试要点速记
  • "四种引用的区别?"——按 GC 回收的"积极程度"排序:强引用(永不) > 软引用(内存不足时) > 弱引用(下次 GC) > 虚引用(随时)
  • "软引用做缓存靠谱吗?"——不够可控,回收时机取决于 JVM 内存状态。生产推荐 Caffeine / Guava Cache
  • "WeakHashMap 和 HashMap 的区别?"——WeakHashMap 的 key 是弱引用,当 key 无外部强引用时,Entry 会被自动清理
  • "ThreadLocal 为什么用弱引用?"——Entry 的 key 用弱引用指向 ThreadLocal 实例,当外部不再引用 ThreadLocal 时,实例可被回收。但 value 仍是强引用,必须手动 remove()
  • "虚引用有什么用?"——跟踪对象被 GC 的时机,用于替代 finalize() 做资源清理。JDK 的 DirectByteBuffer 用 Cleaner(基于 PhantomReference)释放堆外内存

面试回答模板:

"Java 有四种引用类型。强引用是最常见的,GC 绝不回收。软引用在内存不足时被回收,适合做内存敏感的缓存。弱引用在下次 GC 时必定被回收,用于不希望阻止对象回收的关联场景,典型如 WeakHashMap 和 ThreadLocalMap 的 Entry。虚引用无法获取对象,只在对象被回收时通过 ReferenceQueue 通知,JDK 内部用它实现 Cleaner 来释放 DirectByteBuffer 的堆外内存。"