第1章 LeakCanary工作原理:Reference链、GC Root、弱引用、内存快照
1.1 内存泄漏的本质
Java/Kotlin 虚拟机通过 GC Root 可达性分析判定对象是否可回收。如果一个不再使用的对象仍然被 GC Root 通过强引用链间接引用,GC 就无法回收它,造成内存泄漏。LeakCanary 的核心就是反向追踪这条泄漏链条。
1.2 四大关键概念
| 概念 | 定义 | 在 LeakCanary 中的作用 |
|---|---|---|
| GC Root | 堆外引用起点(栈帧、静态字段、JNI 全局引用等) | LeakCanary 从泄漏对象出发,反向追踪到最近的 GC Root |
| 强引用链 | 从 GC Root 到对象的所有强引用路径 | 确定对象为何未被回收;LeakCanary 报告这条最短路径 |
| 弱引用 | 不影响 GC 回收的引用类型 | LeakCanary 在对象销毁时注册弱引用,通过其入队情况判断是否泄漏 |
| 内存快照 | 堆转储文件(.hprof),保存所有对象及其引用关系 | 确认泄漏后自动 dump,用于分析泄漏路径 |
1.3 Reference 链与 GC Root 可达性
GC 从 GC Root 集合出发,遍历所有强引用。无法遍历到的对象即为不可达,可回收。如果某个预期应被销毁的 Activity 仍然可达,则泄漏存在。LeakCanary 捕获泄漏后,使用 haha 库解析 .hprof 文件,计算出从 GC Root 到泄漏对象的最短强引用路径,并去掉弱引用、软引用等干扰。
1.4 弱引用的关键作用
LeakCanary 在 Activity#onDestroy() 后创建一个指向该 Activity 的 WeakReference,并关联一个 ReferenceQueue。当 GC 回收 Activity 时,弱引用会被加入队列。LeakCanary 定期检查队列:如果弱引用未被入队,说明 Activity 仍然被强引用持有,触发 dump。
// LeakCanary 内部简化逻辑
val refQueue = ReferenceQueue<Any>()
val weakRef = WeakReference(activity, refQueue)
// 在后台线程检查
if (refQueue.poll() == null) {
// 弱引用未被回收 → 触发内存快照
heapDumper.dump()
}
💡 关键点:弱引用本身不阻止 GC,因此 LeakCanary 的检测对性能影响极小。但若对象被强引用持有,弱引用会一直有效,从而暴露泄漏。
1.5 内存快照与泄漏链分析
当 LeakCanary 判定对象泄漏后,会调用 Debug.dumpHprofData() 生成 .hprof 文件。随后解析该文件,构建完整的对象关系图,找到从 GC Root 到泄漏对象的最短路径。最终呈现的泄漏链类似:
GC Root (Thread)
└─ com.example.MyActivity (泄露对象)
└─ mStaticField (静态字段)
└─ leakedActivity
⚠️ 性能影响:内存快照操作会暂停应用所有线程(stop-the-world),导致短暂卡顿。LeakCanary 默认仅在应用处于后台或空闲时触发 dump,以降低用户感知。
1.6 整体工作流程
LeakCanary 的检测周期可归纳为以下步骤:
- 监控启动:注册 Activity 生命周期监听。
- 创建弱引用:Activity#onDestroy() 后创建 WeakReference + ReferenceQueue。
- 延迟检查:等待 5 秒(可配置)后检查弱引用是否入队。
- 触发 dump:弱引用未回收 → 调用 Debug.dumpHprofData()。
- 解析快照:遍历 .hprof,找到泄漏对象,计算最短 GC Root 路径。
- 展示报告:通知栏显示泄漏链,点击可查看详情。
🎯 总结:LeakCanary 利用弱引用 + ReferenceQueue 巧妙避开持续监测的消耗,仅在确认泄漏后才生成内存快照并解析引用链。理解 Reference 链、GC Root 和弱引用之间的关系,是正确使用和定制 LeakCanary 的基础。