强引用软引用弱引用虚引用,到底差在哪——我的学习笔记
说在前面:前四篇聊完了 JVM 的内存区域,按计划该聊 GC 了。但我学到 GC 之前发现还有一个东西必须先搞明白——引用类型。因为 GC 回收对象不是"看它有没有人用"这么简单,而是看"用什么类型的引用在用它"。强引用、软引用、弱引用、虚引用这四兄弟,强度不同,GC 对它们的态度也完全不同。这篇就是搞明白它们到底有什么区别。
这个问题是怎么冒出来的
之前学 GC 的时候,我脑子里一直有一个很朴素的想法:如果一个对象没人用了,GC 就把它收掉。
这话听着没错,但它太模糊了。什么叫"没人用"?是没变量指向它就算没人用,还是只有特定类型的变量才算?
后来我看到一段代码:
SoftReference<MyObject>softRef=newSoftReference<>(newMyObject());我当时想:这不就是一个引用包了另一个引用吗?跟直接写MyObject obj = new MyObject()有什么区别?
后来才知道区别大了——区别不在"最终能不能找到这个对象",而在GC 看到这个引用的时候,会不会手下留情。
于是我开始学这四种引用。
一张图看明白四种引用的强度
强引用 > 软引用 > 弱引用 > 虚引用 │ │ │ │ │ │ │ └─ GC 时回收,必须配合 ReferenceQueue │ │ └────────── 下一次 GC 一定回收 │ └─────────────────── 内存溢出前回收 └───────────────────────── 绝不回收从强到弱,GC 的态度从"绝不碰"到"随缘碰"再到"必碰"再到"碰了还要通知你"。
下面一个一个说。
强引用——你最熟悉的一个
MyObjectobj=newMyObject();这就是强引用。我们 99% 的代码写的都是这种。
强引用的规则非常暴力:只要这个引用还在(变量没出作用域、没被置 null),GC 就绝不动它指向的对象。
哪怕 JVM 已经快撑不住了,堆内存马上就要爆了,它也不会去回收强引用指向的对象。它宁愿抛OutOfMemoryError让程序挂掉,也不会动你的强引用。
我之前觉得这条规则太死板了——都快 OOM 了还不收?后来想明白了:设计者把"决定生死"的权利交给了程序员。你自己引用的对象,你自己负责释放。GC 不会自作主张替你收掉。
软引用——内存够就留着,不够就收
软引用比强引用"软"一点。
规则是:内存够用的时候,软引用对象不会被回收。但 JVM 发现内存快不够了,在抛出 OOM 之前,会先把软引用指向的对象收掉。
如果把强引用比作"死也要护着",那软引用就是"能守就守,守不住就算了"。
怎么用
SoftReference<MyObject>softRef=newSoftReference<>(newMyObject());MyObjectobj=softRef.get();// 有可能返回 null注意get()方法——它可能返回 null。因为软引用对象可能已经被 GC 回收了。所以每次使用之前都要判空。
适合做什么
内存敏感缓存。
比如你搞了一个图片缓存,把用户最近看过的图片放在内存里。如果用户不断看新图片,缓存越来越大,但你又不希望这个缓存把堆撑爆——用软引用就合适。内存够的时候缓存正常用,内存紧张的时候 GC 自动把缓存清掉,优先保程序的正常运行。
弱引用——下一次 GC 必收
弱引用比软引用更低一级。
规则是:不管内存够不够,只要发生了 GC,弱引用指向的对象就会被回收。
这就很"无情"了——强引用是"死也不放",软引用是"看情况放",弱引用是"有机会就放"。
怎么用
WeakReference<MyObject>weakRef=newWeakReference<>(newMyObject());MyObjectobj=weakRef.get();// GC 之后返回 null和软引用的区别
这是我当时最困惑的地方——软引用和弱引用到底差在哪?都是回收,不都是 GC 说了算吗?
区别在于触发条件不同:
- 软引用:只在内存要溢出的时候才回收。换句话说是"被动放"
- 弱引用:下一次 GC 就回收,不管内存够不够。换句话说是"主动放"
如果你写了一个缓存,对象存活时间希望长一些(至少在内存不紧张的时候能留着),用软引用。如果你希望对象随时可以被 GC 清掉,不强占内存,用弱引用。
一个经典例子
我当时看到这段代码觉得挺有用的——用弱引用做一个简单的缓存:
importjava.lang.ref.WeakReference;importjava.util.HashMap;importjava.util.Map;publicclassCacheExample{privateMap<String,WeakReference<MyHeavyObject>>cache=newHashMap<>();publicMyHeavyObjectget(Stringkey){WeakReference<MyHeavyObject>ref=cache.get(key);if(ref!=null){returnref.get();// 可能返回 null}else{MyHeavyObjectobj=newMyHeavyObject();cache.put(key,newWeakReference<>(obj));returnobj;}}privatestaticclassMyHeavyObject{privatebyte[]largeData=newbyte[1024*1024*10];// 10MB}}这个缓存的逻辑是:从 Map 里拿对象的时候先判断弱引用还在不在(有没有被 GC 干掉),在就直接用,不在就重新创建。
好处是:其他代码里如果把这个对象的强引用都释放了,GC 下一次扫描就能把这个大对象清掉,缓存不会成为内存泄漏的源头。
但是要注意——每次用get()之前都要判 null。因为这个对象随时可能被回收。初学者(比如我)很容易忘了这一条。
虚引用——最诡异的一个
虚引用是四个里面最诡异的。它的特点我列一下:
get()方法始终返回 null。你拿不到真正的对象。- 必须和
ReferenceQueue一起用。 - 对象被 GC 回收时,虚引用会被放到关联的 ReferenceQueue 里,程序可以通过监控这个队列知道哪些对象被回收了。
我当时看到get()返回 null 的时候很懵——这有什么用?拿都拿不到,那要它干嘛?
后来理解它的用途了:它不是让你拿到对象干活用的,它是让你知道"这个对象什么时候被回收了"。
主要用来做什么
管理堆外内存。
比如 NIO 的DirectByteBuffer就用了虚引用。当 DirectByteBuffer 对象被 GC 回收时,虚引用会被放到队列里,后台线程从队列里取出这个引用,然后释放对应的堆外内存。
堆外内存不受 JVM 堆控制,GC 只管堆里的对象。所以需要一个机制来感知"堆里的那个对象被回收了",然后去清理堆外的内存。虚引用+ReferenceQueue 就是干这个的。
面试常问的一个坑
学完这四种引用之后,我遇到了一个很常见的面试题:
软引用和弱引用有什么区别?
我当时第一反应是:软引用在 OOM 前回收,弱引用下一次 GC 就回收。这个答案没错,但我后来觉得它不够——因为这两个在实际使用中有一个关键的场景区别:
- 软引用适合做缓存:因为你想让缓存对象在内存还够的时候继续活着,只有系统要挂了才放掉
- 弱引用适合做防止内存泄漏的辅助结构:比如
WeakHashMap、ThreadLocal 中的弱引用,目的是不让你的引用成为 GC 的障碍
一个是"保命型",一个是"不碍事型"。这样想会清楚很多。
最后:四种引用对照表
这是我给自己总结的一张速查表:
| 引用类型 | GC 态度 | get() 能否拿到对象 | 常见用途 |
|---|---|---|---|
| 强引用 | 绝不回收 | 能 | 日常写的所有代码 |
| 软引用 | OOM 前才回收 | 可能拿不到(GC 后) | 内存敏感缓存 |
| 弱引用 | 下次 GC 就收 | 可能拿不到(GC 后) | 缓存、防内存泄漏 |
| 虚引用 | GC 时收,入队通知 | 始终拿不到 | 堆外内存管理 |
学完这篇再回头看一开始那个问题——“没人用了就回收”——确实太粗略了。准确的说法应该是:没有强引用指向它了,才有可能被回收。至于多快回收,看这个"没有强引用"是通过哪种引用路径来访问的。
