2、对象生命周期与引用类型:强引用、软引用、弱引用、虚引用在Kotlin中的应用

聊到内存管理,绕不开一个核心问题:对象什么时候该活,什么时候该死?

Java虚拟机有一套垃圾回收机制,但GC不是万能的。它只知道「有没有引用指向这个对象」,却不知道「你心里还想不想用这个对象」。所以,Kotlin(基于JVM)给了我们四种引用类型,让我们能跟GC「商量」一下。

这四种引用,说白了就是四种不同的「牵挂程度」。我当年刚接触时也觉得抽象,直到线上出了OOM,才老老实实把它们啃透。今天咱们一个一个捋清楚。

2.1 强引用——默认的「生死相随」

我们平时写的代码,99%都是强引用:

val user = User("张三")  // 这就是强引用

只要强引用还在,GC就永远不会回收这个对象。哪怕内存爆了,抛出OOM,它也不放手。

这就是双刃剑。好处是简单可靠,坏处是——你忘了置空,它就永远占着内存。

⚠️ 避坑指南
我曾经在某个图片缓存模块里,用静态变量持有了一张大图的强引用。结果用户切后台再回来,App直接闪退。查了半天,就是那张图一直没释放。从那以后,我对静态变量的生命周期格外敏感。

强引用的回收条件只有一个:引用链断开。比如变量超出作用域,或者手动置为null

2.2 软引用——内存够用就留着,不够就回收

软引用适合做「可有可无的缓存」。内存充足时它活着,内存紧张时它先死。

import java.lang.ref.SoftReference

val bitmapRef = SoftReference(bitmap)
val cachedBitmap = bitmapRef.get()  // 可能为null

你想想看,图片缓存用这个是不是很合适?用户滑动列表时,内存够就复用,不够就重新加载,总比OOM强。

我个人的习惯是:二级缓存用软引用。LruCache做一级,软引用池做二级。这样既保证了热点数据的命中率,又不会让冷数据撑爆内存。

💡 小技巧
软引用在Kotlin里用起来有点啰嗦。我一般封装一个委托属性,让调用方感知不到引用的存在。但底层逻辑不变——get()返回null时,说明已经被GC回收了。

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

弱引用比软引用更「薄情」。只要GC一运行,不管内存够不够,弱引用指向的对象都会被回收。

import java.lang.ref.WeakReference

class MyActivity {
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // 处理消息
        }
    }
}

上面这段代码,如果你在Activity里这么写,内存泄漏是板上钉钉的。为什么?因为Handler持有外部类的隐式引用(非静态内部类),而Handler的消息队列又持有Handler。Activity销毁了,但消息还没处理完,Activity就泄漏了。

解决方案?用弱引用:

class MyActivity : AppCompatActivity() {
    private val handler = MyHandler(this)

    private class MyHandler(activity: MyActivity) : Handler(Looper.getMainLooper()) {
        private val activityRef = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            val activity = activityRef.get() ?: return
            // 安全使用activity
        }
    }
}

嗯,这里要注意:弱引用最常见的场景就是打破循环引用。Handler、匿名内部类、观察者模式,这些地方都是重灾区。

📌 核心区别一句话
软引用:内存不够才回收。
弱引用:只要GC就回收。
说白了,弱引用就是「一次性关系」。

2.4 虚引用——最「虚」的引用,几乎等于没有

虚引用是最特殊的一种。你通过虚引用永远拿不到对象本身,它的get()方法始终返回null。

import java.lang.ref.PhantomReference
import java.lang.ref.ReferenceQueue

val queue = ReferenceQueue<ByteArray>()
val phantomRef = PhantomReference(byteArray, queue)
phantomRef.get()  // 永远返回null

那它有什么用?跟踪对象的回收时机

虚引用必须和ReferenceQueue配合使用。当对象被GC回收时,虚引用会被放入队列。你通过监控这个队列,就能知道「哦,那个对象已经死了」。

我在项目中遇到过这样的需求:DirectByteBuffer的回收。DirectByteBuffer分配的是堆外内存,GC管不到。但我们可以通过虚引用监听它的回收,然后手动释放堆外资源。嗯,这个场景比较底层,一般业务开发用不到。

⚠️ 注意
虚引用不能单独使用。你必须持有ReferenceQueue,并且在一个后台线程里轮询队列。否则虚引用就失去了意义。

2.5 四种引用对比

为了方便记忆,我整理了一张表:

引用类型 回收时机 典型场景 能否获取对象
强引用 引用链断开 日常开发
软引用 内存不足时 缓存 能(可能为null)
弱引用 下一次GC 打破循环引用 能(可能为null)
虚引用 任何时候 对象回收跟踪 不能

2.6 Kotlin中的特殊处理

Kotlin在语言层面没有直接提供引用类型,但标准库里有几个工具类值得注意:

  • Lazy:默认是线程安全的懒加载,内部用到了类似弱引用的机制吗?其实没有,它就是个同步锁。但lazy(LazyThreadSafetyMode.NONE)可以避免锁开销。
  • Delegates.observable:观察者模式,注意不要持有外部类的强引用,否则会泄漏。
  • CoroutineScope:协程作用域,内部通过Job管理生命周期。如果你在Activity里用lifecycleScope,它会在销毁时自动取消,这就是弱引用思想的体现。

我个人建议:能用Kotlin协程的,就别自己写Handler了。协程的取消机制天然解决了生命周期问题,比弱引用更优雅。

2.7 实战建议

说了这么多,落地到日常开发,我总结几条铁律:

  1. 静态变量慎用强引用。静态变量的生命周期等于App进程,一旦持有大对象,就是永久内存占用。
  2. 内部类默认持有外部类引用。匿名内部类、非静态内部类,都会隐式持有外部类的this。要么改成静态内部类,要么用弱引用。
  3. 缓存用软引用,但别依赖它。软引用在Android 4.0+之后行为有变化,GC可能更激进。所以软引用适合做二级缓存,不能当主力。
  4. 虚引用别乱用。99%的开发者用不到虚引用。如果你不确定是否需要,那就不需要。
💡 最后说一句
引用类型是JVM层面的东西,Kotlin只是继承了这个机制。但Kotlin的语法糖(如by lazylifecycleScope)让我们更容易写出安全的代码。我的建议是:理解底层原理,善用上层封装