3. 对象引用类型:强引用、软引用、弱引用、虚引用

聊到 GC 抖动,绕不开的一个概念就是——引用类型。你想想看,JVM 判断一个对象能不能回收,依据就是有没有「引用」指向它。但「引用」这玩意儿,也不是铁板一块。Java 里把它分成了四个等级:强引用、软引用、弱引用、虚引用。搞清楚它们,你就能理解为什么有些对象该回收却回收不掉,为什么有些对象一触发 GC 就被清掉。

3.1 强引用——最熟悉的陌生人

强引用就是我们平时写代码最常用的:

MyObject obj = new MyObject();

只要这个引用还在,GC 就永远不会回收它。哪怕内存已经快爆了,抛出 OutOfMemoryError,也不会动强引用指向的对象。

⚠️ 这就是大部分「没释放引用」问题的根源。我曾经排查过一个线上 OOM,查了半天发现是一个全局 Map 里存了业务线程的上下文对象,线程跑完了但 Map 没清,结果所有对象都变成强引用链上的节点,GC 根本动不了它们。

所以,如果你看到老年代持续上涨但 GC 回收不掉,第一个怀疑的对象就是——强引用泄漏。

3.2 软引用——内存够用我就不收

软引用表示「可有可无」的对象。JVM 在发生 OOM 之前,会尝试回收软引用指向的对象。适合做缓存场景:

SoftReference<Bitmap> ref = new SoftReference<>(bitmap);
Bitmap bm = ref.get(); // 返回 null 表示已被回收

我在项目中用软引用实现过一个图片缓存库。刚开始用强引用,结果频繁 OOM;换成软引用后,系统会在内存紧张时自动腾出空间,存活周期刚刚好。

💡 注意:软引用在 GC 日志里会有特殊标记。你如果看到 GC log 里大量 SoftReference 被清除,说明内存压力已经不小了。

3.3 弱引用——活不过下一次 GC

弱引用比软引用更「短命」。只要发生一次 GC(哪怕是 Minor GC),弱引用指向的对象就会被回收。典型的例子是 WeakHashMap 和 ThreadLocal:

WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc(); // 执行后 weakRef.get() 返回 null

ThreadLocal 的内部实现就用到了弱引用——ThreadLocalMap 的 key 是弱引用,这样当 ThreadLocal 不再被外部引用时,GC 能及时回收 key,避免内存泄漏。但我记得有个坑:如果 value 是强引用,key 被回收了但 value 仍然存在,一样会泄漏。所以用完后调 remove() 是必须的。

📌 弱引用的核心价值:打破强引用链,让对象在 GC 时自动消失。常用于数据结构内部(如缓存、ThreadLocal、Listener 注册)。

3.4 虚引用——最透明的影子

虚引用是最弱的引用。你甚至没法通过它获取到对象实例。get() 永远返回 null。它唯一的作用是:对象被回收时,JVM 会把虚引用加入一个引用队列,通知你「这个对象挂了」。主要用于堆外内存管理(如 DirectByteBuffer):

PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);
// obj 被回收后,queue 里会收到这个 PhantomReference

说实话,日常做后台开发,虚引用用得极少。但如果你搞中间件或者 NIO,它是个神奇的「后门」——可以在对象真正被回收前执行一些清理逻辑。

3.5 四种引用对比

引用类型GC 回收时机典型场景个人经验提示
强引用永不回收(直到 OOM)new 对象、集合元素最容易泄漏,检查全局静态集合
软引用OOM 之前缓存、大对象临时存储适合做 LRU 缓存,但注意 GC 延迟
弱引用下一次 GC 立即回收ThreadLocal、WeakHashMap记得清理 value,防止 key/value 不对称泄漏
虚引用对象回收后通知DirectByteBuffer 清理、监控对象销毁很少直接使用,但理解其原理有助于排查堆外内存问题

3.6 引用类型与 GC 抖动的关联

为什么讲这个?因为 GC 抖动经常发生在「该回收的没及时回收」或者「不该回收的反复被回收」的场景。举个例子:

  • 如果缓存用了强引用,内存压力大时不会自动释放,导致老年代持续增长->Full GC 频繁->用户线程暂停。
  • 如果误用了弱引用做缓存,每次 GC 都清掉缓存,然后又大量重新加载,造成 GC 压力不减反增。

我个人习惯:在需要缓存的地方优先考虑软引用,并配合 ReferenceQueue 做些后处理;在需要监听对象生命周期但又不想影响 GC 的地方用弱引用;不要用强引用持有生命周期不确定的对象。

💡 另一个小技巧:排查 GC 问题时,可以用 -XX:+PrintReferenceGC 参数,看 GC 日志里分别回收了多少软、弱、虚引用。如果软引用大量被清,说明内存水位很高;如果弱引用突然激增,可能是某个组件释放了大量临时对象。

Java 引用类型强度与 GC 回收时机 强引用 永不回收(OOM) 软引用 OOM前回收 弱引用 下次GC立即回收 虚引用 引用强度递减 ⚠ 回收时间轴 从永不回收 到 回收后通知 常见问题 • 强引用泄漏 → 老年代暴涨 • 弱引用过度 → 缓存反复加载 • 虚引用队列未处理 → 堆外内存泄漏 • ThreadLocal value未remove → 弱引用陷阱 排查建议 • 用 -XX:+PrintReferenceGC 看引用统计 • heap dump 检查强引用链 箭头方向:引用强度递减,GC 回收概率递增