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出发,经过可达性分析,如果一个对象仍然可达,那就是泄漏。泄漏的根因有三个常见分支。我个人建议你把这个图记在脑子里,以后用MAT分析堆转储时,顺着这个思路找根引用链,定位问题会快很多。
总结几个硬指标
- 静态变量:不要直接持有Activity/Context,用Application或WeakReference。
- 内部类:能用静态内部类就用,lambda里注意捕获。
- 资源:unregister、close、release 一个都不能少。
我在调试BLE通信泄漏时,发现好多同行栽在匿名类回调上。比如BluetoothGattCallback匿名内部类隐式持有Activity,Activity退出了Gatt还没断开——稳如老狗的泄漏。下一篇咱们就专门讲BLE场景下的实战分析。