3. 对象引用类型:强引用、软引用、弱引用、虚引用
聊到 GC 抖动,绕不开的一个概念就是——引用类型。你想想看,JVM 判断一个对象能不能回收,依据就是有没有「引用」指向它。但「引用」这玩意儿,也不是铁板一块。Java 里把它分成了四个等级:强引用、软引用、弱引用、虚引用。搞清楚它们,你就能理解为什么有些对象该回收却回收不掉,为什么有些对象一触发 GC 就被清掉。
3.1 强引用——最熟悉的陌生人
强引用就是我们平时写代码最常用的:
MyObject obj = new MyObject();
只要这个引用还在,GC 就永远不会回收它。哪怕内存已经快爆了,抛出 OutOfMemoryError,也不会动强引用指向的对象。
所以,如果你看到老年代持续上涨但 GC 回收不掉,第一个怀疑的对象就是——强引用泄漏。
3.2 软引用——内存够用我就不收
软引用表示「可有可无」的对象。JVM 在发生 OOM 之前,会尝试回收软引用指向的对象。适合做缓存场景:
SoftReference<Bitmap> ref = new SoftReference<>(bitmap);
Bitmap bm = ref.get(); // 返回 null 表示已被回收
我在项目中用软引用实现过一个图片缓存库。刚开始用强引用,结果频繁 OOM;换成软引用后,系统会在内存紧张时自动腾出空间,存活周期刚刚好。
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() 是必须的。
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 的地方用弱引用;不要用强引用持有生命周期不确定的对象。
-XX:+PrintReferenceGC 参数,看 GC 日志里分别回收了多少软、弱、虚引用。如果软引用大量被清,说明内存水位很高;如果弱引用突然激增,可能是某个组件释放了大量临时对象。