MAT 工具分析堆转储,定位内存泄漏
「MAT 工具分析堆转储,定位内存泄漏」
去年维护一个内容型 App 时,线上 Firebase 突然连续报出 OutOfMemoryError,堆栈集中在 java.lang.OutOfMemoryError: Failed to allocate a 524288 byte allocation with 2097152 free bytes and 2MB until OOM,触发场景毫无规律,有的是在视频详情页退出后,有的是在列表滑动十几屏之后。本地用 LeakCanary 跑了三天 Debug 包,绿灯一片,因为该泄漏只在 Release 混淆包、且配合特定后端下发的缩略图尺寸时出现。最后只能在复现设备上用 adb shell am dumpheap 抓了现场,生成了一个 1.2GB 的 hprof 文件。Android Studio Jellyfish 自带的 Memory Profiler 直接把这个文件拖进去后,索引阶段卡了将近十分钟,点击 Dominator 视图时干脆无响应。无奈之下翻出 Eclipse Memory Analyzer Tool,也就是常说的 MAT,才算把这个锅扣在了代码里一个没清干净的 Runnable 头上。这篇文章想把当时用 MAT 定位问题的完整过程,以及它那些让人又爱又恨的脾气,原原本本地记下来。
抓堆转储与格式转换,的第一步坑
Android 平台抓堆转储的手段不少。Android Studio Profiler 适合在开发阶段可视化抓取,断点式地观察内存走势;LeakCanary 会在检测到泄漏后自动 dump 并裁剪 hprof。但对于线上问题回传或者无法集成 LeakCanary 的场景,adb shell am dumpheap <pid> /data/local/tmp/heap.hprof 依然是最直接的方案。需要注意的是,执行这条命令时应用会出现长达数秒的 Stop-The-World,主线程完全挂起,生产环境谨慎使用。
抓下来的 hprof 文件并不能直接塞进 MAT。Android 的堆转储格式和 Java SE 的标准 hprof 有差异,直接打开会报解析错误。这里必须用 Android SDK platform-tools 目录下的 hprof-conv 工具做一次转换。命令行很简单:hprof-conv heap-original.hprof heap-converted.hprof。如果你用 Android Studio 的 Profiler 抓的,也可以在导出时勾选 "Export to standard .hprof",AS 会自动帮你完成转换,省掉一次命令行操作。不过 AS 导出的文件往往体积更大,因为它不会主动裁剪,这点后面会再提。
MAT 本身是 Eclipse 基金会维护的开源工具,直接去 https://www.eclipse.org/mat/downloads.php 下载即可,完全免费。目前维护中的 1.14 和 1.15 版本对 JDK 17 的支持已经比较完善,但启动前务必修改安装目录下的 MemoryAnalyzer.ini 文件,把 -Xmx 参数从默认的 1024m 往上调。一个 1GB 的 hprof 文件,MAT 解析时可能需要 4GB 甚至更多的堆内存才能流畅运行。如果你在分析过程中看到 MAT 自己也抛出 OutOfMemoryError,大概率是这个启动参数没给够。
Histogram:锁定嫌疑对象的雷达图
MAT 打开标准格式 hprof 后,首先呈现的是 Overview 页,真正的分析从 Histogram 视图开始。Histogram 按类名聚合所有实例,列出每个类的对象数量、Shallow Heap 和 Retained Heap。Shallow Heap 指对象自身所占内存,不包括引用成员;Retained Heap 则是指一旦该对象被回收,连带其所有被支配引用的对象总共能释放多少内存。定位泄漏时,我通常先按 Retained Heap 降序排列,扫一眼哪些自定义类的存在明显违背预期。
当时那个 App 的问题,就是在 Histogram 里看到 com.example.feed.ui.FeedDetailActivity 的实例数有 7 个,而逻辑上当前返回栈里最多只应该存活 1 个。点击类名右键选择 "List Objects" -> "with incoming references",可以看到这 7 个实例各自被谁引用。但这一步只是确认嫌疑,真正定位泄漏源头,需要配合 Dominator Tree 和 Path to GC Roots。
这里有个容易混淆的点:Histogram 里的 Shallow Heap 对 Android Framework 层的对象往往失真。比如一个 Bitmap 对象在 Java Heap 里只占几十字节,但其像素数据在 Android 8.0 之前存放在 Native Heap,8.0