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

  1. 使用 jmap -dump:live,format=b,file=leak.hprof <pid> 或 Android Studio Profiler 导出堆转储文件。
  2. 确保 dump 时间点在泄漏症状稳定出现后,且触发过一次 Full GC(避免过多临时对象干扰)。
  3. 使用 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,绿色节点为泄漏源头,红色节点为泄漏对象。

GlobalScope 滥用 GC Root 路径示意 主线程 (Thread) SystemClassLoader GlobalScope 单例 LaunchCoroutine (未取消) Continuation (匿名闭包) MainActivity 🔍 MAT 分析时选中 Continuation → 右键 "Shortest Path to GC Roots" 即可看到上述引用链。 红线路径表示强引用阻止回收,GlobalScope 单例作为中间桥梁。

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 等短生命周期对象。牢记排除弱引用干扰、重点检查闭包捕获的外部引用,才能在证据采集阶段准确定位泄漏根源。