Perfetto heapprofd 完全指南:如何从火焰图到 SQL 查清堆内存的每一分去向
2026/9/24 15:56:24 网站建设 项目流程

Perfetto heapprofd 完全指南:如何从火焰图到 SQL 查清堆内存的每一分去向

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

Perfetto heapprofd 是一个把进程里每一次堆内存分配与释放都归因到具体 callstack 的采样式分析器,默认盯住malloc/free与 C++ 的new/delete。如果你正在排查"内存越用越多、RSS 一路飙"的问题,它能直接回答谁、在哪条调用栈上、分了多少字节还没还回去——这是 OOM 排查里最难拿到的一手证据。下文按"先上手、后原理、再深挖"的顺序,带你走完从一行命令到 SQL 查询的完整链路。

从一个具体场景开始:内存翻倍了,是谁在分配?

场景:一个工具类 App 跑两天,dumpsys meminfo里 Native Heap 的 Private Dirty 从 50 MB 涨到 110 MB,杀掉进程重启后又回落。重启即恢复,说明不是碎片化,而是有代码在持续"借了不还"。此时 RSS 曲线只能告诉你"涨了",答不了"谁涨的"。heapprofd 的做法是在目标进程内 hook 分配器,把分配事件连同当时完整的调用栈一起记下,事后按 callstack 聚合出"哪条路径的分配没有配对释放"。它的边界也要清楚:只覆盖走默认 C/C++ 分配器的内存,组件直接mmap拿的内存不在它的视野内。

3 分钟上手:heap_profile 一行命令跑通第一条火焰图

结论先行:优先用仓库自带的 tools/heap_profile 脚本,它把配置生成、adb 交互、结果落盘都封装好了。设备用 adb 连上后,按包名发起分析:

tools/heap_profile android -n com.example.myapp

看到 "Profiling active" 就去操作 App(复现你要排查的行为),按 Ctrl-C 结束。脚本会打印输出目录(形如/tmp/heap_profile-XXXXXX,并附带heap_profile-latest软链),把里面的raw-trace上传到 Perfetto UI,时间线上会出现堆转储 slice,点进去就是火焰图。

TIP:分析 App 时把libart.so加进 "Hide Frame" 过滤器,火焰图会聚焦业务代码;顶部 Filters 框还能按函数名聚焦,比如输入applyStyle只看含该帧的调用栈。

按 PID 分析把-n换成-p <pid>;加--print-config可以只打印生成的配置不实际执行,方便自查。全部参数见 heap_profile 命令行参考。

采样原理:hook malloc/free,按 4096 字节做抽检

这节回答:默认配置为什么几乎无感,代价又是什么。heapprofd 在目标进程里 hook 住malloc/freeoperator new/delete,给定采样间隔 n,平均每分配 n 字节才记录一次,默认 n = 4096 字节(对应 CLI 的-i/--interval)。

直观的理解:把进程的全部分配行为看作一条 1 字节的流水,每个字节被"抽中"的概率是 1/n;一旦命中,对应 callstack 记上完整的 n 字节。两条修正规则保证估算不失真:

  • 大于采样间隔的分配绕过抽检,直接按真实大小入账,大对象不会被低估;
  • sampling_interval_bytes设为1即全量采样,精度完美,开销也最大。

另外还有自适应采样:共享内存缓冲剩余空间低于adaptive_sampling_shmem_threshold时,采样间隔自动翻倍,直到adaptive_sampling_max_sampling_interval_bytes封顶;两者置 0 即关闭。更完整的推导见 heapprofd 采样设计文档。

三种启动方式怎么选:脚本、手动 proto 与 UI 录制页

结论:绝大多数场景用脚本就够了;只有"堆分析要和 CPU、调度、日志等数据源在同一条 trace 里并行采集"时,才值得手写配置。

方式适用场景代价
tools/heap_profile(推荐)快速定位单个 App/服务,Android 与 Linux 主机通用单条 trace 只采堆数据
手写HeapprofdConfig堆分析 + 任意其他数据源同 trace 并行自己写并调试 proto
Perfetto UI 录制页(#!/record/memory浏览器零配置实验,Windows 上也可用同样只能单数据源

手动方式的最小 proto(数据源名必须固定为android.heapprofd):

data_sources { config { name: "android.heapprofd" heapprofd_config { sampling_interval_bytes: 4096 process_cmdline: "com.example.myapp" } } }

最常用的字段速览(其余字段去 heapprofd_config.proto 查):

字段说明
sampling_interval_bytes采样间隔(字节)。必须非零,否则 producer 不启动;设 1 = 全量
process_cmdline按进程名匹配;v57+ 支持*通配符(仅匹配运行中进程,需配no_startup
pid按 PID 匹配
heaps要采样的堆名,如libc.malloccom.android.art;留空 = 只采 malloc(Android 12+)
continuous_dump_config周期转储:dump_phase_ms首次转储延迟、dump_interval_ms转储间隔
shmem_size_bytes客户端与 heapprofd 的共享内存缓冲,默认8 MiB;须 ≥8192、4096 的倍数且为 2 的幂
block_client缓冲满时阻塞目标进程而非提前结束 trace(会明显拖慢目标)
no_startup/no_running只匹配运行中进程 / 只匹配新启动进程,二者互斥(Android 11+)

读懂数据:火焰图四种视图、按需快照与连续转储 🔥

这节回答:点进火焰图之后,每个数字到底是什么意思。每次转储对每条 callstack 提供四种口径,UI 的 Measure 选择器里可切换:

视图含义
Unreleased malloc size区间内分配且未释放的字节数(默认视图,查泄漏首选)
Total malloc size区间内分配的总字节数(含已配对释放的)
Unreleased malloc count没有配对释放的分配次数
Total malloc count分配总次数(含已释放的)

除了"会话结束出单个总快照"的默认行为,你手里还有两个控制时间切片的开关:

  • 按需快照adb shell killall -USR1 heapprofd可随时给所有在分析的进程拍一次快照,适合自动化测试里"在特定状态下留一张证据"。它叠加在结束快照之上,可以触发多次,输出目录里按序枚举。
  • 连续转储:命令里加-c 5000(等价于 proto 中continuous_dump_config { dump_interval_ms: 5000 }),UI 里就会每 5 秒出现一个 slice,可拖选多个连续 slice 汇总一个时间窗。

SQL 深挖:分配数据落在哪几张表,怎么查累积分配

这节回答:分配数据到底落在哪几张表。callstack 信息由三张表描述——stack_profile_mapping(库/binary 名与 build_id)、stack_profile_frame(函数名与rel_pc)、stack_profile_callsite(调用点,靠parent_id串成调用树);离线符号化结果存stack_profile_symbol。分配事件本身在heap_profile_allocation视图里,它是 intrinsic 表__intrinsic_heap_profile_allocation的封装:count/size为正表示分配,为负表示释放,二者求和即 Unreleased 口径。

第一段查询把分配明细按帧 join 出来:

select a.callsite_id, a.ts, a.upid, f.name, f.rel_pc, m.build_id, m.name as mapping_name, sum(a.size) as space_size, sum(a.count) as space_count from heap_profile_allocation a join stack_profile_callsite c on (a.callsite_id = c.id) join stack_profile_frame f on (c.frame_id = f.id) join stack_profile_mapping m on (f.mapping = m.id) group by 1, 2, 3, 4, 5, 6, 7 order by space_size desc;

跑完你会发现结果里几乎全是malloc/realloc——叶子帧信息量太低。真正想要的是"某个函数出现在调用栈任意位置时,累计分了多少",而递归追parent_id用纯 SQL 很难写。标准库替你做完了这件事,模块实现见 summary_tree.sql:

INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree; SELECT name, mapping_name AS map_name, cumulative_size FROM android_heap_profile_summary_tree order by abs(cumulative_size) desc;

cumulative_size即"该函数出现在调用栈任意位置时,分配且未释放的字节数",按绝对值降序排,头部几行就是你要找的"谁分掉了这 392 KB"。同目录下的callstacks.sqlintervals.sql还提供调用栈展开与时间窗聚合能力。

进阶话题:启动分析、并发会话互斥与 ART Java 分配

这节收三个容易踩坑、又常被忽略的行为语义。

启动分析 vs 运行时分析:按名字指定目标时,会话启动后新拉起且匹配的进程会从启动即被分析(trace proto 里对应ProcessHeapSamples.from_startup = true);而对已运行进程发请求则不同——hook 要等到下一次分配发生几百毫秒后才真正生效,目标若正空闲,随后的一波分配突发可能被漏掉。Android 上 Java App 不是 exec 出来的,而是从 zygote fork 后特化成具体 App;启动分析从特化点接管,特化最早一小段分配不计入。

并发会话互斥:多个会话盯同一个目标时,只有第一个会话采得到数据;其余会话的ProcessHeapSamples为空且rejected_concurrent = true,转 pprof 时会报"进程已被分析过"。见到这个提示,先adb shell killall perfetto清掉残留会话。

ART Java 分配(Android 12+):给脚本加--heaps com.android.art(等价于 proto 里heaps: "com.android.art")即可改采 Java 堆。

注意:Java 分配分析要求 Android 12+,且别和 Java 堆转储(heap dump)搞混——前者是时间轴上累积的分配 callstack,后者是存活对象快照的保留关系。

ART 样本只记录对象创建时的栈,不跟踪释放与 GC 回收,提供 Total allocation size / Total allocation count 两个视图,适合看内存 churn:哪类代码在疯狂造对象又快速扔掉。另外,Java 方法名带[DEDUPED]表示多个方法共享同一份代码,ART 元数据只存了其中一个方法名——显示出来的未必是实际被调用的那个。

资格与版本坑:先确认你的进程"有资格"被分析

空 profile 的第一嫌疑永远是资格问题。目标能否被分析,取决于 Android 构建类型:

目标类型userdebug (setenforce 0)userdebuguser
关键原生服务YNN
普通原生服务YYN
普通 AppYYN
profileable AppYYY
debuggable AppYYY

user 构建上对无资格进程发请求只会得到空 profile;userdebug 上被禁的是 sepolicy 中never_profile_heap圈定的一小撮关键服务,可用adb shell su root setenforce 0或给脚本传--disable-selinux解除。想让自家 App 在 user 构建上可被分析,在 manifest 的<application>段加<profileable android:shell="true"/>

注意:proto 里的all: true(分析全部合格进程)在未改动的 userdebug 构建上会导致系统崩溃——zygote 拉新进程时撞上意外的 heapprofd socket 直接挂掉,慎用。

已知问题合并成一张版本矩阵(✓ = 该版本存在此问题):

问题A10A11A12A13
64 位设备无法分析 32 位程序
x86/x86_64 平台不支持(含 Cuttlefish 模拟器)
sampling_interval_bytes设为 0 会崩溃目标进程
启动分析部分帧名缺失(A12 起修复)
Java 帧 unwinding 失败,栈顶只剩单个 unknown(A13 QPR1 修复)
子进程 vfork/clone(CLONE_VM) 内分配导致 profile 提前结束(Runtime.exec会触发,可--disable-fork-teardown缓解)
dump_at_max的对象计数可能不准
shmem 过小 +block_client可能卡死目标
每次结束 logcat 出现Failed to send control socket byte.(良性)
带 load bias 的库函数名不准(离线符号化可解)
ARM32 最底层帧恒为ERROR 2(无害,栈仍完整)
root shell 裸跑 heapprofd 使 socket 拿到错误 SELinux 域(restorecon /dev/socket/heapprofd修复)

数字为什么对不上:heapprofd、malloc_info 与 RSS 的口径

这节回答:为什么三个工具报出来的"内存"各说各话。它们量的是不同层面:

指标计量的是什么拿法量级关系
heapprofd目标向默认 C/C++ 分配器请求的字节;不含 zygote 特化前的分配(Java App)、线程缓存与碎片tools/heap_profile最小
malloc_info分配器视角,含线程缓存中"已 free 未归还"的内存,覆盖 zygote 前分配userdebug 上am dumpheap -m <PID> /data/local/tmp/heap.txt居中
Heap RSS分配器向操作系统要到的内存,按页取整 + 碎片浪费;进程被换出到 ZRAM 时反而可能变小adb shell dumpsys meminfo <PID>看 Private Dirty 列通常最大

判断口诀:RSS 或 malloc_info 明显高于 heapprofd,而调用栈上又找不到"漏",大概率是分配器里某种病态碎片化——此时该去审视分配器本身,而不是继续追调用栈。

离开 Android:在 Linux 主机上分析本地进程 🐧

这节讲不连设备怎么分析。从 v58 起,heap_profile host子命令支持直接分析本机 Linux 进程:

tools/heap_profile host -- ./my_binary --some-flag

脚本内部走四步:

  1. 首次运行自动下载traceboxlibheapprofd_glibc_preload.so(linux-amd64 / arm / arm64)到~/.local/share/perfetto/prebuilts/
  2. 通过tracebox --system-sockets拉起内置traced守护进程;
  3. LD_PRELOAD指向 preload 库启动目标,并置PERFETTO_HEAPPROFD_BLOCKING_INIT=1——默认 heapprofd 懒初始化、不阻塞主线程,会漏掉启动期分配;设了该变量后第一次malloc会阻塞到 hook 完全挂好,保证每笔分配都被记到;
  4. 等目标退出(或你按 Ctrl-C),运行trace_processor产出 gzip 压缩的 pprof 与原始 trace,并打印输出目录。

不传-n时进程名默认取--后二进制的 basename。预编译包没有你的平台时,可从源码构建 preload 库再用--preload-library传入。

注意:host子命令只在 Linux 上可用,其他平台会直接报错退出。

排错四件套:缓冲溢出、空 profile、怪调用栈与符号化失败

按出现频率从高到低过一遍。

缓冲溢出(profile 提前结束):分配速率压垮了 heapprofd。若是短暂尖峰,把--shmem-size调大(须为 4096 的倍数、≥8192、2 的幂,脚本会严格校验);若是持续高水位,用--interval=16000或更大值,牺牲精度换存活。

空 profile:先按上一节资格矩阵核对目标是否"有资格",再翻版本坑矩阵;两者都排除后,确认进程真的被匹配到了(名字归一化规则:含/取最后一段、含@截断)。

看起来不可能的调用栈:先查栈里有没有[DEDUPED]帧;其次,若链接时开了 ICF(-Wl,--icf=...),平凡函数(常见如构造/析构函数)可能被别名到毫不相干类的二进制等价体上,栈会"合理但离谱"。

符号化失败:"could not find library"、Build ID 对不上、栈里只剩一帧,都指向离线符号化流程;完整工作流(含传统PERFETTO_BINARY_PATH/PERFETTO_PROGUARD_MAPtrace_processor bundle增强归档)见 符号化文档。

收尾:转 pprof 与"该不该用 heapprofd"选型小结

这节收两个东西:pprof 转换命令,和一张选型判断卡。

tools/trace_processor convert profile /tmp/profile gzip /tmp/heap_profile-XXXXXX/*.pb

得到 gzip 压缩的 pprof profile proto,即可交给各类 pprof 消费工具。

选型小结(3 秒判断你的场景):

  • 想知道"在分配、哪条调用栈没还内存"(native 为主)→ heapprofd;
  • 想量化"Java 对象造得多快、churn在哪"(Android 12+)→ heapprofd +--heaps com.android.art
  • 想回答"此刻哪些存活对象被谁引用着"(泄漏的持有链)→ ART heap dump,流程见 Android 内存排查案例;
  • 怀疑内存走的是裸mmap(驱动、自定义分配器)→ 换 ftrace/perf 抓 mmap 系统调用,heapprofd 看不见这部分;
  • 只想看 RSS 趋势与 OOM 时间线 →dumpsys meminfo+ 内存计数器数据源即可,不必上采样。

数据源全貌与更多实战见 native-heap-profiler 官方文档。

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询