Heap dump 分析实操:主导树、最短 GC 路径、支配树定位泄漏根对象

现象识别

当 App 内存持续增长、GC 无法回收由 Handler 引发的 Activity/Fragment 实例时,典型表现如下:

  • Memory Profiler 中反复触发 GC,Java Heap 曲线仍不回落。
  • Dump 出的 hprof 中 Activity 实例数量异常(远多于预期),且每个实例都持有 Handler 内部类引用。
  • LeakCanary 直接报出 Handler → Activity 的泄漏路径。

数据采集

  1. 触发泄漏场景:旋转屏幕、反复进入退出目标页面,确保泄露对象活跃。
  2. 获取 Heap Dump:
# 使用 Android Studio Profiler 手动 dump
# 或命令行
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof ./dump.hprof
  1. 转换格式:若需 MAT 或第三方工具分析,先转换为标准格式:
hprof-conv dump.hprof dump-converted.hprof

原因定位——三种核心分析手法

从 dump 到手后,按以下故障树逐层深入:

故障树判断规则:先找“疑似泄漏类型”实例数远超基线(如 Activity 本该 1 个,实际有 3+);再对每个实例查最短 GC 路径;若路径中出现 Handler 内部类,则检查其外部类引用字段(this$0)。最后用支配树确认真正无法回收的根对象。

1. 主导树(Dominator Tree)

打开 Android Studio Profiler → “GC Roots” / “内存分析器” 或 MAT → “Dominator Tree”。主导树按对象保留的内存大小降序排列,“支配对象”及其子树就是泄漏根。例如:

对象Shallow HeapRetained Heap是否疑似泄漏
android.os.Handler48 B2.3 MB✅ 保留大量 Activity
com.example.MainActivity384 B1.8 MB✅ 实例数 = 4

若 Handler 对象 retained heap 过大,说明它是支配树中的关键节点。

2. 最短 GC 路径(Shortest GC Path)

在实例上右键 → “Merge Shortest Paths to GC Roots” → “exclude all phantom/weak/soft” 排除软引用。得到类似路径:

Root: Thread (主线程)
 └─ MessageQueue.mMessages (Message)
     └─ Message.target (Handler)
         └─ Handler.this$0 (MainActivity)  ← 泄漏点

排查表:

路径中出现对象判断结论动作
Handler → this$0 → Activity内部 Handler 隐式持有外部引用改用静态内部类 + WeakReference
Message.callback → Runnable → Activitypost 的 Runnable 匿名类持有外部引用同理静态化或者 post 前清除

3. 支配树(Domination Path)

在 MAT 的 Dominator Tree 中选择 Activity 实例,右键 → “Show Path to GC Roots” → “with all references”。结合支配树视图能快速确认真正的根(通常是 Looper 线程或静态变量)。例如:

Looper (main) [GC Root]
 └─ MessageQueue.mMessages
     └─ Message.next
         └─ Message (delay 10min)  ← 未处理,导致 Handler 无法释放
实操技巧:优先用“最短 GC 路径”定位直接持有链,再用“支配树”确认该 Handler 还保留了哪些子树(比如 Bitmap、View 等),避免后续修复遗漏。
Heap Dump 三工具定位泄漏根对象 Heap Dump (.hprof) ① 主导树 按 Retained Heap 降序 找最大支配者 Handler 保留 2.3 MB Activity 实例 4 个 ② 最短 GC 路径 从 GC Root 到实例 定位持有链 Thread → MessageQueue → Message → Handler → Activity ③ 支配树 真正根 + 子树 确认不可回收根 Looper.main 线程 → 所有 pending Message 修复动作 静态 Handler + WeakReference / removeCallbacks 复测 → 预防

修复动作

根据定位结果,采取针对性措施:

  • 内部类 Handler → 静态 + WeakReference
// 修复前:内部类隐式持有 Activity
Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) { … }
};

// 修复后:静态内部类 + WeakReference
static class SafeHandler extends Handler {
    private final WeakReference<MainActivity> mActivityRef;
    SafeHandler(MainActivity activity) {
        mActivityRef = new WeakReference<>(activity);
    }
    @Override
    public void handleMessage(Message msg) {
        MainActivity activity = mActivityRef.get();
        if (activity == null) return;
        // 处理…
    }
}
  • 清理 pending 消息:在 onDestroy 中移除所有回调和消息
@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);  // 清除所有
}
  • 对于 postDelayed:使用 Lifecycle 感知的 Handler,例如 Handler(Looper.getMainLooper()).postDelayed 配合 LifecycleObserver 取消。

复测

修改后重新运行泄漏场景,重复 Heap dump 步骤:

  1. 进入页面 → 旋转 5 次 → 退出。
  2. 手动 GC 后 dump。
  3. 再次打开主导树,确认 Activity 实例数 ≤ 1(或预期数量)。
  4. 最短 GC 路径中不应再出现 Handler → this$0 → Activity 的链。
易遗漏情况:如果泄漏路径不经过 Handler 内部类,而是通过其他静态回调(如 EventBus、RxJava 订阅未取消),则需要额外处理。但 Handler 与 Lifecycle 耦合的典型泄漏均适用上述三工具定位法。

预防

编码规范检查时机备注
禁止使用非静态 Handler 内部类Code Review / LintLint 规则 HandlerLeak
所有 postDelayed 必须在 onStop/onDestroy 取消单元测试 + LeakCanary使用 LifecycleObserver 自动取消
Handler 消息处理中弱引用判空静态分析避免 NullPointerException
定期执行 Heap dump 对比基线CI 流程自动化保留 2 个以上的 Activity 则失败

结合 Android Studio 的“内存分析器模板”可一键生成报告,将上述三个工具的执行步骤做成 排查 Checklist,每次 OOM 问题必走一遍。