强弱软虚四种引用类型

62 阅读4分钟

总览:可达性与回收优先级

强可达(Strong) > 软可达(Soft) > 弱可达(Weak) > 虚可达(Phantom)

引用类型能否通过引用拿到对象何时被回收(大致时机)是否可配合 ReferenceQueue典型用途常见坑
强引用 new Obj()只有在没有任何强引用时才可回收⛔️常规对象持有强引用链导致内存泄漏
软引用 SoftReference✅(get() 可能返回 null)内存吃紧时清理,在 OOM 前必清;也可能被较激进地回收✅(可监听被清理)早期/教科书式“内存敏感缓存”不可靠:不同虚拟机/机型策略不同,Android 上经常被过度清理;不适合作为主缓存(用 LruCache/图片库更稳)
弱引用 WeakReference✅(随时可能变 null)下一次 GC 只要对象没有强/软引用,就会清理避免回调/监听/Handler 持有 Activity/Context;ThreadLocal 的 key容易“还没用就被回收”,不适合做缓存
虚引用 PhantomReference❌(get() 永远返回 null)对象不可到达后,在其真正回收前被入队(用于“临终通知”)✅(必须配合队列才有意义)跟踪对象死亡、释放堆外资源(DirectByteBuffer、JNI 资源);比 finalizer 更安全逻辑复杂;不要用来“复活对象”

关键点:GC 时机由运行时决定,非确定性。以上“时机”是行为准则,不是精确承诺。


1) 强引用(Strong Reference)

  • 定义:普通变量持有对象即强引用。
  • 回收时机:只有当对象不可达(从 GC Roots 走不到)时才会被 GC。
  • 用途:默认就用强引用。
  • :长生命周期对象(单例、静态集合)强持有短生命周期对象(Activity/Fragment/View)会泄漏。

2) 软引用(SoftReference)

  • 特点:get() 可能在任何一次 GC 后变 null;JVM/ART 会在内存压力大时更积极地清理。

  • 回收时机

    • 规范:在抛 OOM 之前必须尽力清理软引用所指对象;
    • 现实:Android(ART/Dalvik)上经常很激进,一 GC 就清;机型/版本差异大。
  • 适用:很少再推荐。不要用作图片/数据缓存的主方案;请用 LruCache、Glide/Coil 的内存缓存。

  • 配合:可注册 ReferenceQueue,被清理时入队以移除相应缓存条目。

val queue = ReferenceQueue<Bitmap>()
val ref = SoftReference(bitmap, queue)
// 轮询 queue,清理你的 map 索引

3) 弱引用(WeakReference)

  • 特点下一次 GC 只要对象没有更强引用,就会被清理,get() 变 null。

  • 回收时机:对象仅被弱引用持有 ⇒ 最近一次 GC 就会回收(很“脆”)。

  • 典型用途(Android)

    • 避免泄漏:回调/监听/Handler 的消息目标指向 Activity 用 WeakReference,不阻止 Activity 回收。
    • 数据结构:WeakHashMap(key 弱引用,key 不再被外界引用时条目自动移除)。
  • 避坑:不适合当缓存(还没来得及用就被 GC 掉)。

class MyHandler(activity: Activity) : Handler(Looper.getMainLooper()) {
    private val actRef = WeakReference(activity)
    override fun handleMessage(msg: Message) {
        val act = actRef.get() ?: return // Activity 已回收,安全退出
        // ...
    }
}

4) 虚引用(PhantomReference)

  • 特点:get() 永远是 null;必须配 ReferenceQueue;对象变成“幻影可达”后入队。

  • 回收时机:对象不可达(无强/软/弱引用)→ GC 标记为可回收 → 在真正回收前将对应 PhantomReference 入队;你在队列里收到“临终通知”。

  • 用途:执行后置清理(尤其是堆外资源):

    • 例如:你包装了一个 ByteBuffer.allocateDirect() / JNI 分配的内存,想在 Java 对象死后确保释放
    • 现代 Java 推荐 Cleaner(Android 也可用),本质上也是基于 Phantom。
  • 避坑:不要企图在这里“复活对象”;也不要做长耗时工作(容易阻塞回收线程)。

class NativeHolder(ptr: Long, queue: ReferenceQueue<NativeHolder>) {
    init { PhantomRegistry.track(this, ptr, queue) }
}
object PhantomRegistry {
    private val map = ConcurrentHashMap<PhantomReference<*>, Long>()
    fun track(target: Any, nativePtr: Long, q: ReferenceQueue<out Any>) {
        val ref = PhantomReference(target, q)
        map[ref] = nativePtr
    }
    // 在后台线程轮询队列:释放堆外资源
    fun loop(queue: ReferenceQueue<out Any>) {
        while (true) {
            val ref = queue.remove() as PhantomReference<*>
            val ptr = map.remove(ref) ?: 0
            if (ptr != 0L) releaseNative(ptr)
            ref.clear()
        }
    }
}

回收判定:可达性分层(直觉版)

  • 强可达:从 GC Roots(线程栈、静态变量、JNI 全局引用等)能走到 ⇒ 绝不回收****
  • 软可达:无强引用,至少有一个 SoftReference 指向 ⇒ 内存紧张优先回收****
  • 弱可达:无强/软引用,至少有 WeakReference 指向 ⇒ 下一次 GC 回收****
  • 虚可达:无强/软/弱,仅有 PhantomReference 关联 ⇒ 入队通知后回收

Android 实战建议(很重要)

  1. 做缓存(图片/数据)不要用 Soft/WeakReference,当心策略不一、体验抖动。

    • 用 LruCache<K, V> 或成熟图片库(Glide/Coil/Picasso)的内存/磁盘缓存。
  2. 避免泄漏:回调、监听器、Handler/Runnable 指向 Activity/Fragment 用 WeakReference 或使持有方与生命周期解耦(lifecycleScope、repeatOnLifecycle)。

  3. 堆外/NDK 资源:优先显式 close()/release();兜底可用 Cleaner/Phantom + ReferenceQueue 做最终清理

  4. ThreadLocal 泄漏意识:ThreadLocalMap 的 key 是弱引用,但 value 不是;线程长寿(线程池)时要 remove()。


什么时候选哪种?

  • 默认:强引用。

  • 解耦但不阻止回收:弱引用(回调/监听/Handler)。

  • 堆外资源兜底清理:虚引用(或 Cleaner),配合 ReferenceQueue 后台回收。

  • 软引用:除非特定 JVM 实验或学习,一般不建议在 Android 用于缓存。