深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用

写 Java 时我们每天都在 new 对象,但很少想过一个问题:这个对象什么时候会被 GC 回收? 答案取决于"谁在引用它,以及引用有多强"。JVM 把引用分为四个等级:强引用 > 软引用 > 弱引用 > 虚引用。下面逐个介绍,每讲完一种就给出对应的测试代码,跑一遍印象更深。


一、强引用(Strong Reference)

最普通的引用,Object obj = new Object() 这种直接赋值就是强引用。

回收时机 :只要强引用还在,GC 就永远不会回收这个对象------哪怕内存快要耗尽,JVM 宁可抛 OutOfMemoryError 也不回收。想回收它,只有一个办法:把引用置为 null 或者让引用离开作用域。

使用场景:日常代码 99% 都是强引用。正因为太常见,我们反而容易忽略它------Android 里最经典的内存泄漏就是 Activity 被 Handler、单例等强引用拽着不放导致的。

测试代码

csharp 复制代码
/** 强引用:只有断开引用(置 null 或离开作用域),对象才能被 GC 回收 */
private static void strongTest() {
    System.out.println("===== 强引用 =====");
    Object obj = new Object(); // 普通的 new 赋值就是强引用
    System.out.println("GC 前(强引用还在):" + obj);
    obj = null; // 断开强引用,对象才有机会被回收
    System.gc();
    System.out.println("置 null 并 GC 后:" + obj);
}

输出:

kotlin 复制代码
===== 强引用 =====
GC 前(强引用还在):java.lang.Object@1b6d3586
置 null 并 GC 后:null

变量 obj 本身是引用,置 null 后不再指向任何对象,原来的对象才能被回收。

原理

GC 判定对象是否存活,本质是看它是否被强引用链(GC Roots)可达。obj = null 切断了从栈到堆中对象的唯一引用链,对象变成不可达,所以 System.gc() 可以回收它。只要强引用链还在,对象即使再也没被业务代码用到,GC 也会把它当"活"的。


二、软引用(SoftReference)

用 SoftReference 包装对象:SoftReference<Object> softRef = new SoftReference<>(obj)。

回收时机 :内存充足 时,GC 不回收它;内存不足时,JVM 在抛出 OOM 之前会先把软引用对象清理掉。可以理解为一种"宁可断尾求生"的兜底策略。

使用场景:内存敏感的缓存。比如图片缓存------内存够用就留着加速访问,内存紧张就自动释放保命。Android 早期很多图片缓存方案就是软引用实现的(现在更推荐 LruCache,因为 LruCache 有明确的淘汰策略,可控性更强)。

注意:通过软引用取对象要用 get() 方法,对象被回收后 get() 返回 null,所以拿到后要做判空。

测试代码

csharp 复制代码
/** 软引用:内存充足时 GC 不回收,内存不足时才回收 */
private static void softTest() {
    System.out.println("===== 软引用 =====");
    Object data = new byte[1024 * 1024]; // 1MB 的对象
    SoftReference<Object> softRef = new SoftReference<>(data);
    data = null; // 断开强引用,此时只剩软引用
​
    System.gc(); // 内存充足,GC 不会动它
    System.out.println("内存充足时 GC 后获取:" + softRef.get()); // 对象还在
​
    // 疯狂申请内存,把堆撑满,模拟内存不足
    try {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 每次 1MB,直到 OOM
        }
    } catch (OutOfMemoryError e) {
        // JVM 抛 OOM 之前,已经先把软引用对象回收了
        System.out.println("内存不足时获取:" + softRef.get()); // null
    }
}

输出 (建议用 -Xmx64m 运行,OOM 触发更快):

kotlin 复制代码
===== 软引用 =====
内存充足时 GC 后获取:[B@1540e19d
内存不足时获取:null

内存够的时候对象安然无恙;堆被撑满后,软引用对象会被 JVM 优先清理。

原理

JVM 在回收软引用时有个"余地":只有当堆剩余空间不足,或者即将 OOM 时,才会清理软引用。内存充足时执行 System.gc(),软引用对象仍然被 SoftReference 内部引用着,所以 get() 能拿到;堆被撑满后,JVM 不得不优先回收软引用对象来避免 OOM,此时 get() 返回 null。


三、弱引用(WeakReference)

用 WeakReference 包装对象:WeakReference<Object> weakRef = new WeakReference<>(obj)。

回收时机:比软引用更"命薄"------只要 GC 一发生,不管内存够不够,只有弱引用可达的对象立刻被回收。它活不过下一次 GC。

使用场景:

  • WeakHashMap:key 是弱引用,外部不再使用该 key 时,entry 自动被清理,适合做"随用随弃"的关联数据。
  • ThreadLocal :ThreadLocalMap 的 key 就是弱引用,防止 ThreadLocal 对象无法回收。
  • Android 防内存泄漏 :用 WeakReference<Activity> 或 WeakReference<Context> 持有引用,既能让内部类访问外部对象,又不妨碍 Activity 被回收。

测试代码

csharp 复制代码
/** 弱引用:只要发生 GC,对象立刻被回收 */
private static void weakTest() {
    System.out.println("===== 弱引用 =====");
    Object data = new Object();
    WeakReference<Object> weakRef = new WeakReference<>(data);
    data = null; // 断开强引用,只剩弱引用
​
    System.out.println("GC 前:" + weakRef.get());
    System.gc();
    System.out.println("GC 后:" + weakRef.get()); // null,弱引用活不过一次 GC
}

输出:

kotlin 复制代码
===== 弱引用 =====
GC 前:java.lang.Object@4554617c
GC 后:null

System.gc() 一执行,只剩弱引用的对象就被回收了,这是和软引用最直观的区别。

原理

弱引用在 GC 的"可达性分析"阶段不会被当作有效的引用链。只要对象没有强引用或软引用可达,只有弱引用指向它,GC 就会直接把它标记为可回收。因此不管内存是否充足,System.gc() 后弱引用对象都会被回收,get() 自然变成 null。


四、虚引用(PhantomReference)

用 PhantomReference 包装对象,且创建时必须 传入一个 ReferenceQueue。

回收时机 :虚引用是"形同虚设"的引用------你永远无法通过它拿到对象,get() 永远返回 null。它唯一的作用是监听对象的死亡 :对象被 GC 回收后,虚引用会被塞进关联的 ReferenceQueue,程序通过轮询队列就能感知"某个对象刚刚死了"。

使用场景 :资源释放的"善后"工作。最典型的是 DirectByteBuffer------堆外内存不受 GC 管理,JVM 就是靠虚引用(Cleaner 机制)感知 Buffer 对象被回收,然后释放对应的堆外内存,防止内存泄漏。

测试代码

csharp 复制代码
/** 虚引用:get() 永远返回 null,只能配合 ReferenceQueue 感知对象被回收的时机 */
private static void phantomTest() throws InterruptedException {
    System.out.println("===== 虚引用 =====");
    Object data = new Object();
    ReferenceQueue<Object> queue = new ReferenceQueue<>();
    PhantomReference<Object> phantomRef = new PhantomReference<>(data, queue);
    data = null;
​
    System.out.println("GC 前 get():" + phantomRef.get()); // 永远是 null
    System.gc();
    Thread.sleep(500); // 稍等 GC 把虚引用塞进队列
​
    Reference<?> ref = queue.poll(); // 从队列取出"已死亡对象"的虚引用
    System.out.println("对象回收后,引用队列中拿到:" + ref);
    System.out.println("是当初注册的那个虚引用吗?" + (ref == phantomRef));
}

输出:

csharp 复制代码
===== 虚引用 =====
GC 前 get():null
对象回收后,引用队列中拿到:java.lang.ref.PhantomReference@74a14482
是当初注册的那个虚引用吗?true

拿不到对象本身,但对象被回收后,当初注册的虚引用会出现在引用队列里------这就是它的"报丧"机制。

原理

虚引用设计上就不允许通过它访问对象(get() 固定返回 null),这是为了避免你"复活"一个即将被回收的对象。它的价值在对象被回收的"后半段":GC 在回收对象前,会把关联的虚引用注册到 ReferenceQueue。业务线程通过 poll() 拿到这个信号后,就可以做清理工作,比如释放堆外内存、关闭文件句柄等。

相关推荐
恋猫de小郭1 小时前
Flutter Golden Tests:给 AI Agent 的 UI 测试系统
android·前端·flutter
zbmwa1 小时前
jetpack compose 副作用 SideEffect
android jetpack
知昂七昂1 小时前
AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?
android
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo6 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077006 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android