一条命令三步定位内存泄漏:Perfetto heapprofd 实战完整指南
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
你有没有遇到过这种情况:应用在用户手里用半小时越来越卡,最后被系统杀掉,而你自己的测试机上完全复现不出来?内存类崩溃是 Android 线上最顽固的问题之一——系统内存吃紧时,lmkd 会按 oom_score_adj 从高到低杀掉缓存中的应用,而一个每次操作只泄漏 2MB 的缺陷,30 次操作后就能吃掉 60MB,足以让你的 App 成为第一批被杀的对象。Perfetto 的 heapprofd(采样式 native 内存剖析器)可以在真机上拦截 malloc/free 调用,把每一笔未释放的内存归因到具体调用栈,采样间隔默认 4096 字节,对前台应用几乎无感。本文用 3 条命令,带你走完"录制—读图—修复验证"的全流程。
一个复现不出来的泄漏:商品列表越滑越吃内存
先讲一个典型的排查现场。某资讯类 App 收到用户反馈:连续下滑信息流 20 分钟后应用被杀,重启后继续。开发环境用 LeakCanary 跑了 30 分钟,Java 堆没有增长,查无实据。问题在于:LeakCanary 只看 Java 对象引用,而内存增长的元凶是 native 侧——dumpsys meminfo显示每次滚动 10 次,Native Heap 的 Private Dirty 稳定上涨约 2MB,Java 堆纹丝不动。
排查分三步走:
- 确认增长曲线:用 Perfetto 录制 30 秒内存计数器,滚动列表,观察
mem.rss.anon(进程匿名内存,即真正的物理占用)是否单调上升; - 归因到调用栈:用 heapprofd 录制同一段操作,看火焰图里哪条调用路径的"未释放内存"在涨;
- 定位代码:火焰图里锁定到离屏渲染缩略图的 EGL 路径——每个缩略图新建了渲染上下文却从未销毁,1.6MB 的纹理缓冲跟着上下文一起被挂住。
这个案例的关键启示:Java 堆干净不代表没有泄漏,native 分配(哪怕你一行 C++ 都没写,JNI 背后的框架 API 也会走 malloc)必须单独查。
录制第一条 native 内存 profile:最简 heap_profile 命令
前置条件:设备 Android 10+,已连接 ADB;在 user 版本系统上,目标 App 的 manifest 需要带<profileable android:shell="true"/>或 debuggable 标记,否则 profile 会是空的。仓库里没有现成脚本的话,先 clone 一份:
git clone https://gitcode.com/GitHub_Trending/pe/perfetto cd perfetto下面这条命令录制 30 秒(-d单位是毫秒)的 native 堆 profile,每 5 秒出一个快照(-c 5000),结束后在/tmp/heap_profile-latest下生成 raw-trace:
tools/heap_profile android -n com.example.news -d 30000 -c 5000录制期间反复执行你的复现路径(滚动、切换页面),然后 Ctrl-C 结束。输出里会出现Wrote profiles to /tmp/... (symlink /tmp/heap_profile-latest),把它指向的raw-trace文件拖进 Perfetto UI 的网页前端即可查看。
图1:Perfetto UI 录制页的 Native heap profiling 配置项:目标进程名、采样间隔(默认 4096B)、连续快照间隔
⚠️新手最大的误区:以为 heapprofd 能看到"之前"的分配。它是非回溯式的——只统计开始录制之后发生的 malloc/free,启动时就已经存在的内存它一概不知。所以如果你的问题是"进程现在为什么这么大",heapprofd 回答不了;只有当泄漏会随时间持续发生时(绝大多数泄漏都是),才能靠观察增量抓住它。另一个隐蔽的坑:多个录制会话指向同一个进程时只有第一个生效,如果 profile 为空,先
adb shell killall perfetto清掉残留会话再重试。
读懂火焰图:未释放内存的 4 个度量维度
打开 raw-trace,点时间轴上菱形标记的 "Native heap profile" 切片,默认展示 Unreleased Malloc Size 火焰图:横轴是宽度(大小),顶层是main()/pthread_start等入口,越往下越接近最终的malloc调用点。根节点标注的总 MiB 就是本次记录期内"分配了但没 free 掉"的总量。
图2:Native heap profile 火焰图:默认按 Unreleased Malloc Size 聚合未释放内存,根节点显示 46.31 MiB 总量
四个度量模式各查各的病(火焰图左上角可切换):
- Unreleased Malloc Size(默认):未释放字节数,查泄漏主战场;
- Unreleased Malloc Count:未释放分配次数,专抓"单个很小、数量巨大"的碎片泄漏;
- Total Malloc Size:含已释放的总分配量,查分配热点(churn),比如频繁创建销毁的临时对象;
- Total Malloc Count:总分配次数,看 allocator 压力。
开了-c连续快照后,时间轴上会排开多个切片,每个切片对应 5 秒窗口;选中相邻的多个切片还能做窗口合并对比,泄漏路径会随滑动次数明显变宽:
图3:开启 -c 5000 连续快照后,时间轴上按时间窗排列的多个 Native heap profile 切片
图形看不爽就上 SQL。Perfetto 把堆数据存进标准表,UI 的 Query (SQL) 标签页或命令行trace_processor query都能跑:
tools/trace_processor query /tmp/heap_profile-latest/raw-trace \ "INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree; SELECT name, self_size, cumulative_size FROM android_heap_profile_summary_tree WHERE cumulative_size > 1048576 ORDER BY cumulative_size DESC LIMIT 10;"self_size是该帧作为叶子时的未释放字节数,cumulative_size是出现在任意层级的未释放字节数——排序后 Top 10 基本就是修复优先级表。
图4:UI 的 Query (SQL) 窗口:上半区写 PerfettoSQL,下半区看结果表
案例闭环:两轮录制拿到修复前后的对比数据
回到信息流案例。修复前的量化基线(30 秒滚动,5 个连续快照):
- Native Heap PSS:48MB → 110MB,净增 62MB;
- 火焰图
eglCreateContext路径 cumulative_size 约 60.9MB,且每个快照切片比上一个宽 2MB 左右; mem.rss.anon计数器单调上行,无回落。
修复动作:把 EGL 上下文收敛为进程内单例复用,销毁缩略图时同步释放纹理。修复后用完全相同的命令和复现路径再录一次(这是闭环的关键——变量只有一个,就是代码):
tools/heap_profile android -n com.example.news -d 30000 -c 5000对比结果:同样 30 秒滚动,Native Heap PSS 48MB → 51.8MB,净增 3.8MB(其中 2.9MB 是缓存正常水位),eglCreateContext路径 cumulative_size 从 60.9MB 降到 4.2MB,降幅 93%;mem.rss.anon曲线在滑动停止后回落到基线附近。
图5:mem.rss.anon 等内存计数器时间线:RSS 尖峰与用户操作时间点一一对应,是验证泄漏是否复发的直观依据
工具选型:Java 堆、native 堆、内核内存各查哪份数据
Perfetto 的内存数据源不止 heapprofd,选错工具会白忙一场。按"你想回答什么问题"对号入座:
| 你要回答的问题 | 数据源 | 最低版本 | 触发方式 |
|---|---|---|---|
| native 内存去哪了 / 谁在泄漏(malloc 级) | heapprofd native profile | Android 10 | tools/heap_profile android |
| Java/Kotlin 对象被谁引用着 | ART heap dump | Android 11 | tools/java_heap_dump -n <进程名> |
| Java 对象分配太频繁(churn) | ART allocation profiling | Android 12 | heap_profile加--heaps com.android.art |
| 进程 RSS/swap 随时间的走势 | 内存 ftrace 计数器 | 内核 tracepoint | ftracekmem/rss_stat等事件 |
| 为什么被 lmkd 杀了 | oom_score_adj/lowmemory_kill | 内核 tracepoint | ftrace 事件 +linux.process_stats |
| 绕过 malloc 的直接 mmap 大块 | syscall 事件 / perf 全采样 | Android 14 / 12 | syscall_events: "sys_mmap"或 perfperiod: 1 |
两点补充:Java 堆 dump 是整张对象引用图的一次性快照(支持回溯,但看不到分配调用点),和 heapprofd 正好互补,细节见 Java 堆剖析文档 与 内存案例手册。文件映射内存(dex、so)用adb shell showmap <PID>更直接,Perfetto 管不了这部分。
图6:ART heap dump 在 UI 中的火焰图视图:按最短可达路径聚合 Java 对象占用,可切换支配树视角
进阶:自定义分配器、采样权衡与后台场景
- 自定义分配器对 heapprofd 不可见。如果你的 App 有自己的内存池(图片缓存、解码器 buffer 常见),用 heapprofd Custom Allocator API 把池子注册进来,分配/释放时各报一笔,就能进火焰图:
#include "heap_profile.h" static uint32_t g_heap = AHeapProfile_registerHeap( AHeapInfo_create("example.image_cache")); void* my_malloc(size_t size) { void* ptr = real_malloc(size); AHeapProfile_reportAllocation(g_heap, (uintptr_t)ptr, size); return ptr; // 释放侧对应调用 AHeapProfile_reportFree }- 采样间隔是精度和开销的旋钮:
-i默认 4096,即平均每分配 4KiB 记一笔;前台交互型 App 保持默认即可,后台长时录制可以调到 8192 减半开销。大于间隔的大块分配不走采样,按真实大小记录,所以大泄漏不会因为采样被稀释。 - 后台与低端设备场景:录制时 App 退后台,
mem.rss.anon会告诉你它是否被 ZRAM 换出;被 lmkd 杀掉的进程在lowmemory_kill事件里有时间戳,可以和泄漏曲线对出"死亡时刻"。这些配置都基于 ftrace,见 memory 案例手册。
上线前内存健康自检清单
把下面这些项塞进你的发布流程,泄漏就不用等到线上:
录制前
- user 构建的 App 已打
profileable android:shell="true"标记 - 复现路径写成可重复操作的脚本化步骤(滚动 30 次 / 切换 10 个页面)
- 已
adb shell killall perfetto排除残留会话
录制中
- 连续快照间隔 ≤ 操作周期,至少 5 个切片用于看趋势
- 前台敏感场景未调低采样间隔导致数据稀疏
读图时
- Unreleased 和 Total 两个维度都看过(区分泄漏 vs churn)
- 火焰图里对
libart.so加了 Hide Frame 过滤,减少噪音 - SQL 里用
cumulative_size排过 Top 10,和图上看到的最大块对得上
修复后
- 用同一命令、同一复现路径重录,对比 Native Heap 增量(要求降幅 ≥90% 或给出解释)
- 低内存设备(4GB RAM)上跑通完整复现路径,
mem.rss.anon无单调爬升
最后几条实践建议
- 先定基线再谈优化:没有修复前 30 秒的量化录制,任何"优化后内存降了"都不可信。
- native 和 Java 两条线并行查:
dumpsys meminfo的 Private Dirty 分属两类堆,LeakCanary 只管得住后者。 - 把 heap_profile 接进 CI:
trace_processor query是无界面输出的,泄漏回归可以做成一条卡发布门槛的自动化。 - 火焰图配合代码读:最大的块往往在公共路径上,真正要修的是它下面第二、第三深的叶子帧。
💡专业提示:把-c 5000的连续快照作为日常巡检标配,每次发版前跑一次并存档 raw-trace;用cumulative_sizeTop 10 做版本间 diff,能在用户报障前两周就抓到缓慢增长的新泄漏,修复成本比线上救火低一个数量级。更多 SQL 写法见 PerfettoSQL 入门,命令行细节见 trace_processor 参考 与 heap_profile 参考。
【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考