堆转储基础:概念、触发时机与 GC Roots 的关系

现象识别 – 什么样的问题需要堆转储?

堆转储(Heap Dump)是排查内存泄漏、内存抖动和 OOM 的第一手证据。当应用出现以下红牌现象时,应果断采集堆转储:

  • 内存持续增长:反复进入/离开某界面后,android.os.Debug.MemoryInfo 显示 PSS 只升不降。
  • 频繁 GC:Logcat 中连续出现 Background young concurrent copying GC,UI 掉帧。
  • OOM 崩溃:java.lang.OutOfMemoryError,且堆栈指向 Bitmap 或 Activity 相关代码。
  • ANR 且堆栈卡在分配:dalvik.system.VMRuntime.runFinalization 或 java.lang.ThreadLocal 阻塞。
判断规则: 只要存在 两个以上 上述现象,优先抓取堆转储,而非盲目怀疑某个类。

数据采集 – 如何正确触发堆转储?

堆转储是 Java 堆对象及其引用关系的 “快照”。Android 提供三种标准触发方式:

  1. Android Profiler(推荐):打开 Memory Profiler,在需要的时间点点击 “Dump Java Heap” 按钮。生成的 .hprof 文件会自动转换为标准格式。
  2. ADB 命令:适合自动化或没有 UI 的场景。
  3. 代码主动触发:用于特定业务节点(如打开特定页面后)。
# 方式一:adb 触发堆转储,pid 可通过 "adb shell ps | grep 包名" 获取
adb shell am dumpheap <pid> /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof .

# 方式二:在 Activity 关键生命周期触发(仅测试代码)
Debug.dumpHprofData("/sdcard/activity_leak.hprof");
避坑: 直接从 Android Profiler 导出的 .hprof 文件是 Dalvik 格式,若要在 MAT 或 Eclipse Memory Analyzer 中分析,需先通过 hprof-conv 转换。Android Studio 会自动完成转换,但若手动转换:
cd $ANDROID_HOME/tools/
./hprof-conv raw.hprof standard.hprof
堆转储触发场景对比表
触发方式适用场景优势劣势
Android Profiler开发阶段,UI 可视化直接查看支配树、引用链需连接 Studio,对大堆转储加载慢
ADB 命令自动化测试、线上问题复现无需 IDE,可脚本批量采集文件需手动拉取,无实时预览
代码 Debug.dumpHprofData()特定业务节点(如长列表加载后)时机精确,可配合日志侵入性强,发布版需去除

原因定位 – 堆转储与 GC Roots 的关系

堆转储文件保存了对象之间的引用关系图。GC Roots 是垃圾回收的起点,包括:活动线程、静态变量、JNI 全局引用、正在执行的方法中的局部变量等。

内存泄漏的本质是:某些不再需要的对象,因为被一个或多个 GC Roots 直接或间接引用,导致无法被回收。分析过程就是:

  1. 在堆转储中找出疑似泄漏的对象(如 Activity 数量异常多)。
  2. 查看该对象的 GC Roots 路径(Path to GC Roots)。
  3. 沿着最短路径找到未被清理的 root 持有者,定位代码位置。
# 使用 Android Studio Profiler 手动定位泄漏
1. 打开堆转储文件,选择 "Analyzer Tasks" -> "Detect Leaked Activities"
2. 查看报告中的 "Leaked Activity" 列表
3. 双击某个 Activity,点击 "Path to GC Roots" -> 选择 "exclude weak references"
4. 查看最短路径:如 A -> B -> C -> 静态 sInstance,即 sInstance 持有了 Activity
关键规则: 如果某个对象存在 多于一条 的 GC Roots 路径,则说明它被多个线程或静态域引用,泄漏概率极高。应优先分析最短且非弱引用的路径。

修复动作 – 根据 GC Roots 路径切断引用

切断引用链的标准做法取决于 root 类型:

GC Roots 类型常见泄漏场景修复方案
static 变量单例持有了 Activity Context改用 Application Context,或在 onDestroy 中置 null
Thread / AsyncTask匿名内部类隐式引用外部 Activity使用静态内部类 + WeakReference,或使用协程 + 生命周期感知
Listener / Observer注册后未反注册在 onPause / onDestroy 中反注册
Handler / MessageQueueHandler 持有 Activity,Message 未被处理使用静态 Handler + Activity weakreference,或 removeCallbacksAndMessages

修复后,务必清理 IDE 或手动置空不必要的静态引用。

复测 – 如何确认修复有效?

复测必须模拟相同场景,并对比修复前后的堆转储:

  1. 重复之前引起泄漏的操作流程(例如反复进入/离开 Activity 15 次)。
  2. 在相同时机触发堆转储。
  3. 观察目标对象个数:修复前可能看到 15 个 Activity 实例,修复后应只有 1 个(当前展示的)。
  4. 检查 GC Roots 路径是否已消失。
# 快速对比脚本(bash)
adb shell am dumpheap <pid> /data/local/tmp/after_fix.hprof
adb pull /data/local/tmp/after_fix.hprof .
# 在 Android Studio 中打开 after_fix.hprof,使用 “Compare Two Heap Dumps” 功能对比
复测验收标准: 未修复的泄漏对象 必须降为零,或仅剩正常生命周期内的活对象。

预防 – 建立堆转储分析的文化与工具

堆转储分析不应仅用于事后救火,更要嵌入日常开发流程:

  • CI/CD 自动化:在 Pull Request 的 UI 测试中自动触发堆转储,对比基线基线 Mem 上限。
  • 定期快照:每个大版本发布前,用 adb shell dumpsys meminfo 与堆转储交叉检查。
  • 团队规则:所有静态变量持有 Context 必须通过 代码 Review + 堆转储验证。
堆转储分析 → 泄漏排查流程 现象识别 触发堆转储 分析 GC Roots 路径 修复切断引用 复测对比堆转储 预防 & 自动化 故障树梳理: OOM/内存增长 → 触发 Dump → 观察 Dominator Tree 中 Retained Heap 最大对象 → 查看 “Path to GC Roots(exclude weak)”→ 识别 Root 类型 → 切断引用 → 复测