Heap dump 分析实操:主导树、最短 GC 路径、支配树定位泄漏根对象
现象识别
当 App 内存持续增长、GC 无法回收由 Handler 引发的 Activity/Fragment 实例时,典型表现如下:
- Memory Profiler 中反复触发 GC,Java Heap 曲线仍不回落。
- Dump 出的 hprof 中 Activity 实例数量异常(远多于预期),且每个实例都持有
Handler内部类引用。 - LeakCanary 直接报出
Handler→Activity的泄漏路径。
数据采集
- 触发泄漏场景:旋转屏幕、反复进入退出目标页面,确保泄露对象活跃。
- 获取 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
- 转换格式:若需 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 Heap | Retained Heap | 是否疑似泄漏 |
|---|---|---|---|
| android.os.Handler | 48 B | 2.3 MB | ✅ 保留大量 Activity |
| com.example.MainActivity | 384 B | 1.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 → Activity | post 的 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 等),避免后续修复遗漏。
修复动作
根据定位结果,采取针对性措施:
- 内部类 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 步骤:
- 进入页面 → 旋转 5 次 → 退出。
- 手动 GC 后 dump。
- 再次打开主导树,确认 Activity 实例数 ≤ 1(或预期数量)。
- 最短 GC 路径中不应再出现 Handler → this$0 → Activity 的链。
易遗漏情况:如果泄漏路径不经过 Handler 内部类,而是通过其他静态回调(如 EventBus、RxJava 订阅未取消),则需要额外处理。但 Handler 与 Lifecycle 耦合的典型泄漏均适用上述三工具定位法。
预防
| 编码规范 | 检查时机 | 备注 |
|---|---|---|
| 禁止使用非静态 Handler 内部类 | Code Review / Lint | Lint 规则 HandlerLeak |
| 所有 postDelayed 必须在 onStop/onDestroy 取消 | 单元测试 + LeakCanary | 使用 LifecycleObserver 自动取消 |
| Handler 消息处理中弱引用判空 | 静态分析 | 避免 NullPointerException |
| 定期执行 Heap dump 对比基线 | CI 流程 | 自动化保留 2 个以上的 Activity 则失败 |
结合 Android Studio 的“内存分析器模板”可一键生成报告,将上述三个工具的执行步骤做成 排查 Checklist,每次 OOM 问题必走一遍。