说在前面前四篇聊完了 JVM 的内存区域按计划该聊 GC 了。但我学到 GC 之前发现还有一个东西必须先搞明白——引用类型。因为 GC 回收对象不是看它有没有人用这么简单而是看用什么类型的引用在用它。强引用、软引用、弱引用、虚引用这四兄弟强度不同GC 对它们的态度也完全不同。这篇就是搞明白它们到底有什么区别。这个问题是怎么冒出来的之前学 GC 的时候我脑子里一直有一个很朴素的想法如果一个对象没人用了GC 就把它收掉。这话听着没错但它太模糊了。什么叫没人用是没变量指向它就算没人用还是只有特定类型的变量才算后来我看到一段代码SoftReferenceMyObjectsoftRefnewSoftReference(newMyObject());我当时想这不就是一个引用包了另一个引用吗跟直接写MyObject obj new MyObject()有什么区别后来才知道区别大了——区别不在最终能不能找到这个对象而在GC 看到这个引用的时候会不会手下留情。于是我开始学这四种引用。一张图看明白四种引用的强度强引用 软引用 弱引用 虚引用 │ │ │ │ │ │ │ └─ GC 时回收必须配合 ReferenceQueue │ │ └────────── 下一次 GC 一定回收 │ └─────────────────── 内存溢出前回收 └───────────────────────── 绝不回收从强到弱GC 的态度从绝不碰到随缘碰再到必碰再到碰了还要通知你。下面一个一个说。强引用——你最熟悉的一个MyObjectobjnewMyObject();这就是强引用。我们 99% 的代码写的都是这种。强引用的规则非常暴力只要这个引用还在变量没出作用域、没被置 nullGC 就绝不动它指向的对象。哪怕 JVM 已经快撑不住了堆内存马上就要爆了它也不会去回收强引用指向的对象。它宁愿抛OutOfMemoryError让程序挂掉也不会动你的强引用。我之前觉得这条规则太死板了——都快 OOM 了还不收后来想明白了设计者把决定生死的权利交给了程序员。你自己引用的对象你自己负责释放。GC 不会自作主张替你收掉。软引用——内存够就留着不够就收软引用比强引用软一点。规则是内存够用的时候软引用对象不会被回收。但 JVM 发现内存快不够了在抛出 OOM 之前会先把软引用指向的对象收掉。如果把强引用比作死也要护着那软引用就是能守就守守不住就算了。怎么用SoftReferenceMyObjectsoftRefnewSoftReference(newMyObject());MyObjectobjsoftRef.get();// 有可能返回 null注意get()方法——它可能返回 null。因为软引用对象可能已经被 GC 回收了。所以每次使用之前都要判空。适合做什么内存敏感缓存。比如你搞了一个图片缓存把用户最近看过的图片放在内存里。如果用户不断看新图片缓存越来越大但你又不希望这个缓存把堆撑爆——用软引用就合适。内存够的时候缓存正常用内存紧张的时候 GC 自动把缓存清掉优先保程序的正常运行。弱引用——下一次 GC 必收弱引用比软引用更低一级。规则是不管内存够不够只要发生了 GC弱引用指向的对象就会被回收。这就很无情了——强引用是死也不放软引用是看情况放弱引用是有机会就放。怎么用WeakReferenceMyObjectweakRefnewWeakReference(newMyObject());MyObjectobjweakRef.get();// GC 之后返回 null和软引用的区别这是我当时最困惑的地方——软引用和弱引用到底差在哪都是回收不都是 GC 说了算吗区别在于触发条件不同软引用只在内存要溢出的时候才回收。换句话说是被动放弱引用下一次 GC 就回收不管内存够不够。换句话说是主动放如果你写了一个缓存对象存活时间希望长一些至少在内存不紧张的时候能留着用软引用。如果你希望对象随时可以被 GC 清掉不强占内存用弱引用。一个经典例子我当时看到这段代码觉得挺有用的——用弱引用做一个简单的缓存importjava.lang.ref.WeakReference;importjava.util.HashMap;importjava.util.Map;publicclassCacheExample{privateMapString,WeakReferenceMyHeavyObjectcachenewHashMap();publicMyHeavyObjectget(Stringkey){WeakReferenceMyHeavyObjectrefcache.get(key);if(ref!null){returnref.get();// 可能返回 null}else{MyHeavyObjectobjnewMyHeavyObject();cache.put(key,newWeakReference(obj));returnobj;}}privatestaticclassMyHeavyObject{privatebyte[]largeDatanewbyte[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 时收入队通知始终拿不到堆外内存管理学完这篇再回头看一开始那个问题——“没人用了就回收”——确实太粗略了。准确的说法应该是没有强引用指向它了才有可能被回收。至于多快回收看这个没有强引用是通过哪种引用路径来访问的。