4. 泄漏证据采集:使用 MAT 分析 GC Root 路径
4.1 为什么需要追踪 GC Root 路径?
当 Heap Dump 中观察到大量 GlobalScope 关联对象存活时,仅凭总量无法确认泄漏源头。必须从 GC Root 出发,沿着引用链找到阻止这些对象被回收的“根引用”。MAT(Memory Analyzer Tool)提供了自动泄漏嫌疑扫描和手动 GC Root 路径分析两种模式。本章将演示如何利用 MAT 定位 GlobalScope 滥用导致的泄漏证据。
4.2 数据采集:导出 Heap Dump
- 使用
jmap -dump:live,format=b,file=leak.hprof <pid>或 Android Studio Profiler 导出堆转储文件。 - 确保 dump 时间点在泄漏症状稳定出现后,且触发过一次 Full GC(避免过多临时对象干扰)。
- 使用 Eclipse Memory Analyzer(MAT)打开 .hprof 文件,选择“Leak Suspects Report”或直接进入“Heap Dump Overview”。
🚀 实用提示:若需分析全量对象(包含弱可达/软可达),移除
-live 选项。但在多数泄漏场景中,-live 更利于聚焦。
4.3 MAT 分析步骤:从全局视图到 GC Root 路径
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 在 MAT 中打开 dump → 选择 “Leak Suspects” 向导 | 自动列出最可能泄漏的对象及保留大小 |
| 2 | 展开嫌疑条目,点击 “See stacktrace” 或 “Path to GC Roots” | 显示从 GC Root 到嫌疑对象的最短引用链 |
| 3 | 若嫌疑人列表不直达,使用 “Histogram” 视图查找 kotlinx.coroutines.Job 或 kotlinx.coroutines.internal.ScopeCoroutine 实例 |
获得所有相关对象的实例数量 |
| 4 | 右键对象 → “Merge Shortest Paths to GC Roots” → 选择排除软/弱引用(通常只保留强引用) | 得到精确的强引用 GC Root 路径 |
4.4 识别 GlobalScope 滥用路径:典型 GC Root 链
以下是一条在真实案例中截取到的 GC Root 路径(简化后):
GC Root: Thread (main) → local variable
→ GlobalScope (kotlinx.coroutines.EmptyCoroutineContext)
→ LaunchCoroutine (parentJob = null)
→ Continuation at MainActivity$onCreate$1 (匿名类)
→ outerClass (MainActivity)
→ leakBitmap (静态变量或匿名内部类引用)
关键模式:
- GC Root 往往是主线程或任何长期活跃的线程(因为 GlobalScope 本身被
GlobalScope单例持有,而单例又通过kotlinx.coroutines.CoroutineScopeKt.GlobalScope静态属性关联到SystemClassLoader)。 - 启动的协程未取消 → 协程的 Continuation 强引用外部的 Activity 或 Fragment → 导致整个 Activity 不能回收。
- 闭包捕获了 Context、View 或大对象 → 在协程存活期间,这些对象也被归入 GlobalScope 的子树。
💡 判定规则:如果在 GC Root 路径中连续出现
GlobalScope → LaunchCoroutine → Continuation,且 Continuation 的引用链中包含 Activity / View / Fragment 等非静态对象,即可判定 GlobalScope 滥用导致的内存泄漏。对比正常 scope(如 viewModelScope),其 GC Root 应为 ViewModel 或指定的生命周期对象,不会强制存活。
4.5 实例分析:MAT 可视化 GC Root 路径
下面这张 SVG 图展示了一条典型的 GlobalScope 泄漏引用链,以及 MAT 中如何呈现节点间的引用关系。图中深色节点为 GC Roots,绿色节点为泄漏源头,红色节点为泄漏对象。
4.6 必须避开的误判陷阱
- 弱/软引用干扰:MAT GC Root 视图默认包含软引用、弱引用,导致路径虚长。在弹出菜单中必须取消勾选 “exclude phantoms/soft references” 才能看到真正阻碍回收的强引用链。
- 单例对象是否真实泄漏:
GlobalScope单例本身是静态的,只要协程内部没有引用外部对象,就不会泄漏。需要重点检查 GC Root 路径中是否包含 Activity / Fragment / Bitmap / Context 等非长期存活的组件。 - 跨协程引用:一个协程未取消可能导致多层嵌套,此时应使用
Incoming References视图逐级展开。
4.7 快速排查清单
| 检查项 | 操作 | 结论 |
|---|---|---|
1. 是否存在 GlobalScope 强引用路径 | 搜索类名 kotlinx.coroutines.GlobalScope 查看实例及引用 | 有 ⇒ 进一步分析;无 ⇒ 检查其他 scope |
| 2. 协程未取消的 Continuation | 筛选 kotlin.coroutines.Continuation 子类,查看 outgoing references | 若指向外部对象则标记泄漏 |
| 3. 外部对象大小 | 计算保留集大小(Retained Set) | 如果占用 > 10MB 且属于泄漏嫌疑,确定为泄漏 |
4. 重复启动协程的 launch 点 | 双击 Continuation 查看堆栈,定位源代码 | 找到未取消的 GlobalScope.launch 调用 |
4.8 本章小结
通过 MAT 的 GC Root 路径分析,可以可视化地证实 GlobalScope 滥用导致的泄漏。核心步骤包括:导出 Heap Dump → 使用 Leak Suspects 或直连 Histogram → 筛选 Continuation 类 → 合并最短路径 → 确认强引用链中包含 Activity 等短生命周期对象。牢记排除弱引用干扰、重点检查闭包捕获的外部引用,才能在证据采集阶段准确定位泄漏根源。