2、内存泄漏基础:什么是内存泄漏、Java/Kotlin垃圾回收机制、常见泄漏类型

做内存分析这些年,我遇到过不少「明明代码逻辑没问题,内存却一直在涨」的case。说实话,大部分刚入门的同学对内存泄漏的理解就停留在「对象该回收没回收」这句空话上。今天咱们把这块掰开揉碎了聊清楚。

什么是内存泄漏

简单说,就是堆里有些对象已经没用了,但还被某些「僵尸引用」拽着,GC没法回收。时间一长,可用内存越来越少,最终触发OOM。你想想看,一个Activity退出了,按理说它的整个对象图都应该被回收,但要是被某个静态变量持有,它就永远躺在堆里——这就是泄漏。

核心判定标准:对象不再被程序使用,却被GC Roots链可达。

Java / Kotlin 垃圾回收机制

要理解泄漏,得先懂GC的干活逻辑。Java和Kotlin跑在JVM上,主流都是分代收集,但底层判断对象「死活」的关键算法是——可达性分析。

GC从一组叫 GC Roots 的根节点出发,通过引用链向下遍历。所有不可达的对象就是垃圾。嗯,这里要注意:不是引用计数,是根搜索。Kotlin在JVM层和Java共享同一套GC,只是语法糖(比如lambda、协程)会生成一些隐式对象,这些对象也可能成为泄漏源头。

常见泄漏类型

我根据自己的踩坑经历,把最常见的三种拎出来讲:

泄漏类型典型场景GC能回收吗
静态变量持有静态集合里存Activity❌ 一直被Root引用
内部类/匿名类Handler、Runnable持有外部类❌ 隐式this引用
资源未释放广播、Cursor、Socket未close❌ 底层fd泄漏

下面展开说说每种泄漏的根因和代码表现。

静态变量泄漏

Kotlin里用companion object或object声明的单例,本质上就是静态变量。如果你不小心把Activity实例塞进静态集合里——

object GlobalCache {
    val activities = mutableListOf<Activity>()
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(...) {
        GlobalCache.activities.add(this) // 泄漏!
    }
}

这个Activity退出后,GlobalCache作为静态对象是GC Root,它引用了Activity,Activity就永远不会被回收。我曾经在一个老项目里看到有人用静态HashMap存所有打开的Activity实例,说是为了「方便统一管理」——结果线上OOM率飙升。

避坑指南:静态变量尽量用弱引用(WeakReference)或应用级对象持有,别直接存Activity/Fragment。

内部类 / 匿名类泄漏

Kotlin里内部类默认持有外部类的引用(用inner关键字)。如果你在Activity里定义一个Handler:

class MyActivity : AppCompatActivity() {
    inner class MyHandler : Handler() {
        override fun handleMessage(msg: Message) {
            // ...
        }
    }

    private val handler = MyHandler()

    override fun onCreate(...) {
        handler.sendEmptyMessageDelayed(0, 60000) // 延迟消息
    }
}

Handler里的This$0隐式引用了外部Activity。即使Activity finish了,只要消息队列里还有延迟消息(60秒后),Activity就不会被回收。同理,Kotlin的lambda表达式如果捕获了外部类的属性,也会生成匿名内部类,要注意生命周期。

解决思路:用静态内部类 + WeakReference,或者直接用Lifecycle-aware组件。我个人习惯在onDestroy里移除所有回调。

资源未释放泄漏

资源泄漏往往不是Java堆内存泄漏,而是Native内存或文件句柄泄漏。比如注册BroadcastReceiver后忘记unregister:

class MyActivity : AppCompatActivity() {
    private val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) { }
    }

    override fun onResume() {
        registerReceiver(receiver, IntentFilter("action"))
    }

    // 忘了在onPause里 unregisterReceiver(receiver)
}

每次Activity重建都会重新注册,系统会持有一个对该Receiver的强引用,进而导致Activity泄漏。还有Cursor、Socket、FileInputStream……这些资源如果不在finally或use块里关闭,也是一样的后果。

Kotlin提供了.use扩展方法,我建议养成习惯:

FileInputStream(file).use { fis ->
    fis.read()
} // 自动close

一张图看懂泄漏链条

内存泄漏核心概念图 GC Roots 可达性分析 (遍历引用链) 不可达 → 正常回收 可达 → 内存泄漏 静态变量持有 内部类/匿名类 资源未释放 泄漏链:GC Roots → 可达性 → 对象引用未被切断 → 无法回收

这张图把泄漏的全链路画出来了:GC Roots出发,经过可达性分析,如果一个对象仍然可达,那就是泄漏。泄漏的根因有三个常见分支。我个人建议你把这个图记在脑子里,以后用MAT分析堆转储时,顺着这个思路找根引用链,定位问题会快很多。

总结几个硬指标

  • 静态变量:不要直接持有Activity/Context,用Application或WeakReference。
  • 内部类:能用静态内部类就用,lambda里注意捕获。
  • 资源:unregister、close、release 一个都不能少。

我在调试BLE通信泄漏时,发现好多同行栽在匿名类回调上。比如BluetoothGattCallback匿名内部类隐式持有Activity,Activity退出了Gatt还没断开——稳如老狗的泄漏。下一篇咱们就专门讲BLE场景下的实战分析。