Perfetto 原生堆内存分析(Heapprofd)完全指南:从采样原理到火焰图与 SQL 分析

发布时间:2026/9/17 18:38:23
Perfetto 原生堆内存分析(Heapprofd)完全指南:从采样原理到火焰图与 SQL 分析 Perfetto 原生堆内存分析Heapprofd完全指南从采样原理到火焰图与 SQL 分析【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto本指南以 Perfetto 项目中的 heapprofd 内存分析工具为核心系统讲解如何在 AndroidAndroid 10与非 Android Linux 主机上对进程进行基于调用栈callstack的堆内存分配/释放跟踪。读完本文你将掌握三种启动方式手动配置、tools/heap_profile脚本、Perfetto UI 录制页、采样与连续转储机制、ART Java 分配分析Android 12、SQL 查询调用栈分配、pprof 转换以及各类故障排查方法。heapprofd 的完整文档位于 docs/data-sources/native-heap-profiler.md其配置结构定义在 protos/perfetto/config/profiling/heapprofd_config.proto。Heapprofd 是什么Heapprofd 是 Perfetto 中用于跟踪 Android 进程在给定时间段内堆内存分配allocation与释放deallocation的工具。生成的 profile 可以把内存占用归因到具体的调用栈callstacks同时支持原生native代码与 Java 代码的混合场景Android 平台开发者与应用开发者都可以用它来排查内存问题。默认行为记录通过malloc/free或 C 的new/delete完成的原生分配与释放。可配置行为通过heaps: com.android.art改为记录 Java 堆内存分配见下文 ART Allocation Profiling。系统要求heapprofd 要求 Android 10 或更高版本Java 分配分析要求 Android 12 或更高版本。权限边界在 debug 版 Android 构建上可以分析所有应用及大多数系统服务在 user 版正式发布、不可 root构建上只能分析带debuggable或profileablemanifest 标志的应用。上手速成可参考 docs/case-studies/memory.md 中的 heapprofd 章节。UI 查看火焰图与时间线切片Heapprofd 的转储在 Perfetto UI 中显示为火焰图点击时间线上对应的 slice即可查看该 slice 生命周期内收集到的分配与调用栈汇总。下图分别展示了 UI 轨道中的 heapprofd 快照切片与对应的火焰图。每次分析会话得到的 profile proto 中每个 slice 都包含四类视图Unreleased malloc size该调用栈在 slice 持续期间分配但未释放的字节数。Total malloc size该调用栈在 slice 持续期间分配的字节总数包含已配对释放的部分。Unreleased malloc count该调用栈上无配对释放的分配次数。Total malloc count该调用栈上的分配总次数含已配对释放的。TIP分析应用时可以给 Hide Frame 过滤器加上libart.so过滤掉 ART 运行时的内部帧让火焰图聚焦于业务代码。SQL 查询分配数据落库到哪些表调用栈相关信息写入以下三张表均为 Perfetto Trace Processor 的堆分析标准表stack_profile_mapping映射二进制/库信息如 build_id、名称stack_profile_frame帧信息如函数名、相对 PCrel_pcstack_profile_callsite调用点callsite通过parent_id构成调用树。分配本身写入heap_profile_allocation表在 src/trace_processor/perfetto_sql/stdlib/prelude/after_eof/views.sql 中可以看到它是对底层 intrinsic 表__intrinsic_heap_profile_allocation的视图封装。离线符号化数据存放在stack_profile_symbol表。具体示例查询见下文「示例 SQL 查询」一节。录制三种启动方式Heapprofd 可以通过三种方式配置并启动。手动配置Manual configuration手动在 trace config 中设置HeapprofdConfig字段完整字段定义见 protos/perfetto/config/profiling/heapprofd_config.proto。这样做唯一的好处是可以在同一条 trace 中与任何其他 tracing 数据源如 CPU、调度、日志并行开启堆分析。典型的最小配置如下data_sources { config { name: android.heapprofd heapprofd_config { sampling_interval_bytes: 4096 process_cmdline: com.example.myapp } } }核心字段速览均为 proto2 可选字段字段类型说明sampling_interval_bytesuint64采样间隔字节必须为非零值否则 producer 不会启动设为 1 即全量记录process_cmdlinerepeated string按进程名匹配支持 v57 的*通配符通配符仅匹配运行中进程需同时设置no_startuppidrepeated uint64按 PID 匹配用于 watermark 触发或本地调试heapsrepeated string要采样的堆名如libc.malloc、com.android.art留空则只采样mallocAndroid 12all_heapsbool采样目标注册的全部堆Android 12heap_sampling_intervalsrepeated uint64与heaps一一对应的采样间隔未给出时使用sampling_interval_bytescontinuous_dump_configmessage周期转储配置dump_phase_ms首转储延迟、dump_interval_ms转储间隔shmem_size_bytesuint64客户端与 heapprofd 之间的共享内存缓冲默认 8 MiB需 ≥8192、为 4096 的倍数且是 2 的幂超过 500 MiB 会被截断block_clientbool缓冲满时阻塞客户端而非提前结束 trace会显著拖慢目标进程block_client_timeout_usuint32阻塞客户端超时需 100usAndroid 11no_startup/no_runningbool只匹配运行中进程 / 只匹配启动进程二者互斥Android 11dump_at_maxbool在采样堆用量达到峰值时转储Android 11min_anonymous_memory_kbuint32跳过 anon RSS swap 低于该值的进程Android 11max_heapprofd_memory_kb/max_heapprofd_cpu_secsuint32/uint64超过则停止 profileAndroid 11allbool分析系统上所有符合资格进程注意在未修改的 userdebug 构建上会导致系统崩溃Zygote 启动新进程时会因意外的 heapprofd socket 而崩溃target_installed_byrepeated string只分析由指定包安装的目标特殊值system/product/nullAndroid 12skip_symbol_prefixrepeated string对匹配前缀的 mapping 不输出函数名如/system使用 tools/heap_profile 脚本推荐tools/heap_profile是最推荐的启动方式脚本源码位于 tools/heap_profile。它有两个子命令heap_profile android通过adb分析已连接 Android 设备上的进程若不给子命令则默认走此分支保持历史调用方式。heap_profile host分析本地 Linux 进程见下文「非 Android Linux 支持」。Android 场景下可以按名称-n com.example.myapp或按 PID-p 1234指定目标。按名称匹配时profile 会对已经运行且匹配包名的进程以及会话启动后新启动的匹配进程同时生效后者即启动分析。常用参数完整清单见 docs/reference/heap_profile-cli.mdtools/heap_profile android -n com.example.myapp # 按名称分析 tools/heap_profile android -p 1234 # 按 PID 分析 tools/heap_profile android -n com.example.myapp -c 5000 # 每 5000ms 连续转储 tools/heap_profile android -n system_server --print-config # 仅打印配置不执行脚本会把raw-trace文件输出到指定目录默认一个临时目录上传到 Perfetto UI 即可看到时间线上所有的堆转储 slice点击任意 slice 查看火焰图。脚本内部会校验--shmem-size必须是 4096 的倍数、不小于 8192 且为 2 的幂见 tools/heap_profile否则直接 FATAL 退出。使用 Perfetto UI 录制页在 Perfetto UI 的录制页#!/record/memory勾选 Native heap profiling输入目标进程点击 Connect new device 配对手机后即可直接从浏览器录制 profileWindows 上也支持。这种方式无需手动编写 trace config适合快速实验。连续转储Continuous dumps默认情况下堆分析器从录制开始捕获全部分配只在结束时保存单个快照UI 中显示为单个 slice汇总了期间的所有分配/释放。可以配置为周期性而非仅在 trace 结束时保存快照例如每 5000ms 一次三种等价做法UI 中将 Continuous dump interval 设为 5000在HeapprofdConfig中增加continuous_dump_config { dump_interval_ms: 5000 }在tools/heap_profile android或tools/heap_profile host调用中加-c 5000。连续转储的 UI 上会出现多个 slice点击每个 slice 查看该时间段内的分配/释放汇总也可以拖拽框选多个连续 slice汇总该时间窗口内的分配情况。采样间隔原理Heapprofd 通过 hookmalloc/free以及 C 的operator new/delete来采样堆分配。给定采样间隔 n 字节平均每分配 n 字节采样一次从而降低对目标进程的性能影响。默认采样率为 4096 字节对应 CLI 参数-i/--interval的默认值脚本与 proto 中的sampling_interval_bytes一致。最直观的理解方式把内存分配想象成连续的 1 字节流其中每个字节以 1/n 的概率被选为样本被选中的字节对应的调用栈被记上完整的 n 字节。大于采样间隔的分配会绕过采样逻辑按真实大小记录以保证大对象不被低估。sampling_interval_bytes设为 1 即可获得完美精度全量采样。此外proto 中还提供了自适应采样adaptive sampling能力当共享内存缓冲剩余空间低于adaptive_sampling_shmem_threshold时采样间隔翻倍最高不超过adaptive_sampling_max_sampling_interval_bytes两者为 0 时禁用。详细原理见 docs/design-docs/heapprofd-sampling.md。启动分析Startup profiling按进程名称而非 PID指定目标时新启动且匹配该名称的进程会从启动开始即被分析。生成的 profile 包含从进程启动到分析会话结束之间的所有分配。Android 上 Java 应用通常不是从零exec()而是从 [zygote] fork 后特化specialize为具体应用。如果应用名匹配分析会话中的名称分析会在 zygote 特化阶段启用。因此 profile 包含从该特化点开始到会话结束的所有分配特化早期的一小部分分配不会被计入。在 trace proto 层面对应的ProcessHeapSamples消息会置from_startup字段为 trueProfilePacket定义见 docs/reference/trace-packet-proto.autogen.md但该字段不会体现在转换为 pprof 兼容 proto 的输出中。运行时分析Runtime profiling分析会话启动时所有匹配进程按名称或 PID会被枚举并被通知请求分析。但分析实际上不会立即启用——要等应用下一次分配发生后几百毫秒才真正生效。如果应用在请求分析时处于空闲随后才发生分配突发这些突发分配可能会被漏掉。生成的 profile 包含从分析启用时刻到会话结束之间的所有分配。对应ProcessHeapSamples中from_startup为 false同样不会体现在 pprof 兼容 proto 中。并发分析会话如果多个会话指定了同一个目标进程按名称或 PID只有第一个相关会话能分析该进程其余会话在转换为 pprof 兼容 proto 时会报告进程已被分析过。如果看到该提示但不确定有别的会话可以运行adb shell killall perfetto终止可能正在运行的并发会话。被拒绝的会话在ProfilePacket中对应一个内容为空的ProcessHeapSamples消息其rejected_concurrent字段为 true同样不体现在 pprof 兼容 proto 中。目标进程的资格限制取决于运行 heapprofd 的 Android 构建类型部分进程不能被分析详见下文表格user正式发布、不可 root构建只有带profileable或debuggablemanifest 标志的 Java 应用可被分析。对不可分析进程发起请求会得到空 profile。userdebug 构建除了一小撮关键服务外几乎所有进程都可分析。被禁止的目标由 SELinux 策略中的never_profile_heap规则system/sepolicy 中的 heapprofd.te定义。可以通过adb shell su root setenforce 0关闭 SELinux或给脚本传--disable-selinux解除限制。目标类型userdebug setenforce 0userdebuguser关键原生服务critical native serviceYNN原生服务native serviceYYN应用appYYNprofileable 应用YYYdebuggable 应用YYY要把应用标记为 profileable在应用 manifest 的application段中加入profileable android:shelltrue/manifest ... application profileable android:shelltrue/ ... /application /manifestART 分配分析Churn ProfilingJava 分配注意Java 分配分析要求Android 12 或更高版本。 注意Java 分配分析不要与Java 堆转储Heap dumps 混淆。Heapprofd 也可以配置为跟踪 Java 分配而非原生分配两种等价做法在HeapprofdConfig中加heaps: com.android.art给tools/heap_profile android加--heaps com.android.art。与 ART 堆转储显示存活对象快照的保留图 retention graph不同ART 分配采样与原生堆 profile 类似展示的是整个 profile 期间按时间累积的分配调用栈。ART 分配样本只显示对象创建时的调用栈不显示删除或 GC 回收时的调用栈。ART 分配样本提供两种视图Total allocation size截至当前时刻该调用栈上分配的字节总数。字节可能已被释放也可能没有工具不跟踪释放。Total allocation count截至当前时刻该调用栈上分配的对象总数。对象可能已被释放也可能没有工具不跟踪释放。ART 分配样本对理解**内存 churn内存颠簸/频繁分配**非常有用它展示大额分配应归因于哪部分代码的调用栈以及 ART 运行时的分配类型。DEDUPED 帧如果 Java 方法名中包含[DEDUPED]说明有多个方法共享同一份代码。ART 的元数据只保存其中单个方法的名称即这里显示的名字显示出来的不一定是实际被调用的那个方法。按需触发堆快照堆快照会在两种时机写入 trace使用continuous_dump_config时按固定时间间隔否则在会话结束时。还可以通过如下命令随时触发所有正在分析进程的一次快照adb shell killall -USR1 heapprofd这在实验室测试中很有用——可以在目标处于特定状态时记录其当前内存占用。该转储会叠加在始终会产生的会话结束转储之上可以多次触发输出目录中会依次枚举这些转储文件。符号化与反混淆Symbolization Deobfuscation如果 profile 显示的是原始地址或混淆后的 Java/Kotlin 名称可以对采集的 trace 运行trace_processor bundle生成增强归档。完整的离线符号化工作流包括传统的PERFETTO_BINARY_PATH/PERFETTO_PROGUARD_MAP方式见 docs/learning-more/symbolization.md。故障排查Buffer overrun缓冲溢出如果分配速率过高导致 heapprofd 跟不上分析会话会因缓冲溢出提前结束。若溢出由短暂的分配尖峰引起增大共享内存缓冲给tools/heap_profile android/tools/heap_profile host传--shmem-size即可解决。否则需要增大采样间隔以牺牲精度为代价例如传--interval16000或更大。对应 proto 字段即上文的shmem_size_bytes与sampling_interval_bytes若使用block_client模式缓冲满时客户端会被阻塞等待空间可配合block_client_timeout_us设置阻塞超时。Profile 为空先对照上表确认目标进程是否具备被分析的资格再检查下方「已知问题」。不合理的调用栈Implausible callstacks如果看到从代码逻辑上不可能出现的调用栈先确认没有 DEDUPED 帧 参与。此外如果代码使用Identical Code FoldingICF链接即给链接器传-Wl,--icf...大多数平凡函数常见如构造函数和析构函数会被别名到完全不相关类的二进制等价函数上。符号化问题遇到 could not find library、Build ID 不匹配、只显示一帧only one frame shown等问题见 docs/learning-more/symbolization.md 的 troubleshooting 小节。非 AndroidLinux 支持从 Perfetto v58 起tools/heap_profile支持分析本地 Linux 进程无需 Android 设备tools/heap_profile host -- ./my_binary --some-flag脚本执行过程源码见 tools/heap_profile首次运行自动下载tracebox与libheapprofd_glibc_preload.solinux-amd64 / arm / arm64 预编译产物清单见脚本内嵌的 manifest到~/.local/share/perfetto/prebuilts/通过tracebox --system-sockets启动内置的traced守护进程以LD_PRELOAD指向 preload 库、并设置PERFETTO_HEAPPROFD_BLOCKING_INIT1启动目标二进制。默认 heapprofd 是懒初始化以不阻塞主线程这会导致启动阶段的分配被漏掉设置该环境变量后第一次malloc会阻塞直到 heapprofd 完全挂接从而保证每个分配都被正确跟踪脚本中对应PERFETTO_HEAPPROFD_BLOCKING_INIT1的调用等待目标退出或你按Ctrl-C然后运行trace_processor生成 gzip 压缩的 pprof 文件以及原始 trace。如果不传-n/--name进程名默认为--之后二进制名的 basename。运行结束脚本会打印输出目录Wrote profiles to /tmp/heap_profile-XXXXXX (symlink /tmp/heap_profile-latest) The raw-trace and heap_dump.* (pprof) files can be visualized with https://ui.perfetto.dev.把raw-trace上传到 Perfetto UI 即可可视化。注意host子命令只在 Linux 上运行其他平台会报错退出。使用自编译的 preload 库如果预编译产物还不支持你的平台可以从 Perfetto 源码构建该库构建说明见 docs/contributing/build-instructions.md再通过--preload-library传入tools/setup_all_configs.py tools/ninja -C out/linux_clang_release heapprofd_glibc_preload tools/heap_profile host \ --preload-library out/linux_clang_release/libheapprofd_glibc_preload.so \ -- ./my_binary --some-flag已知问题按 Android 版本Android 13Java 帧的 unwinding 可能无法正常工作取决于所用的 ART 模块版本。此情况下 UI 会在栈顶报告单个 unknown 帧。该问题在 Android 13 QPR1 中修复。Android 12Java 帧的 unwinding 可能无法正常工作取决于所用的 ART 模块版本UI 会在栈顶报告单个 unknown 帧。Android 1164 位设备上无法分析 32 位程序。将sampling_interval_bytes设为 0 会崩溃目标进程这是应被拒绝的非法配置proto 注释也明确警告了该 BUG。启动分析时部分帧名可能缺失Android 12 中修复。每次 profile 结束时 logcat 中会显示Failed to send control socket byte.这是良性信息。dump_at_maxprofile 中对象计数可能不正确。共享内存缓冲设得过低且使用block_client模式可能导致目标进程卡死。Android 10带 load bias 的库中函数名可能不正确可用离线符号化解决。启动分析时部分帧名可能缺失Android 12 中修复。64 位设备上无法分析 32 位程序。不支持 x86 / x86_64 平台包括 Android Cuttlefish 模拟器。ARM32 上最底层帧总是ERROR 2这是无害的调用栈依然完整。如果 heapprofd 独立运行在 root shell 中直接运行heapprofd而非通过 init/dev/socket/heapprofd会被分配错误的 SELinux domain导致无法分析任何进程除非禁用 SELinux enforcement。在 root shell 中运行restorecon /dev/socket/heapprofd可解决。在子进程中使用vfork(2)或带CLONE_VM的clone(2)并分配/释放内存会提前结束 profile。java.lang.Runtime.exec就是如此调用它会提前结束 profile注意这违反了 POSIX 标准。可在 CLI 中通过--disable-fork-teardown缓解。将sampling_interval_bytes设为 0 会崩溃目标进程。每次 profile 结束时 logcat 中显示Failed to send control socket byte.良性。dump_at_maxprofile 中对象计数可能不正确。共享内存缓冲过低且使用block_client模式可能卡死目标进程。Heapprofd vs malloc_info() vs RSS理解 heapprofd 结果时必须清楚操作系统可提供的几种内存指标的确切含义heapprofd目标程序向默认 C/C 分配器请求的字节数。如果从启动分析一个 Java 应用应用初始化早期发生的分配对 heapprofd 不可见不从 Zygote fork 的原生服务不受此影响。malloc_infolibc 提供的分配器信息函数。在 userdebug 构建上可用am dumpheap -m PID /data/local/tmp/heap.txt触发。由于分配器不会立刻释放所有内存该值通常大于 heapprofd 看到的值特别是 jemalloc 会在线程缓存thread caches中保留部分已释放内存。Heap RSS分配器向操作系统请求的内存量。因为内存只能按页大小获取且碎片化会造成浪费该值通常大于前两者。可用adb shell dumpsys meminfo PID查看 Private Dirty 列获得。如果设备内核启用了内存压缩ZRAM较新 Android 版本默认开启且进程内存被换出到 ZRAMRSS 也可能小于前两者。指标维度heapprofdmalloc_infoRSS原生启动from native startupxxxZygote 初始化之后after zygote initxxxZygote 初始化之前before zygote initxx线程缓存thread cachesxx碎片化fragmentationx如果观察到高 RSS 或高 malloc_info 指标而 heapprofd 数值不匹配可能正遭遇分配器中某种病态的碎片化问题。转换为 pprof 格式可以使用trace_processor把 trace 中的堆转储转换为 [pprof] 格式tools/trace_processor convert profile /tmp/profile这会在/tmp/下创建包含堆转储的目录然后执行gzip /tmp/heap_profile-XXXXXX/*.pb得到 gzip 压缩的 proto——这是处理 pprof profile proto 的工具所期望的格式。trace_processor convert的用法详见 docs/quickstart/traceconv.md。示例 SQL 查询在 Trace Processor 中可以用 SQL 查询得到执行分配的调用栈。对每个帧count和size为正的行表示分配的字节数若有已被释放的分配则会出现count/size为负的对应行。两者求和即为上文所述的Unreleased malloc size视图。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;示例输出节选callsite_idtsupidnamerel_pcbuild_idmapping_namespace_sizespace_count666051malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so106496419251malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so266241142151malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so266241153751malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so266241884351malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so264241861851malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so245764375051malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so122881282051malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so81922378851malloc2447168126fd../apex/com.android.runtime/lib64/bionic/libc.so81922可以看到函数名全是malloc/realloc信息量有限——通常我们关心某个函数的**累积cumulative**分配字节数。要递归追踪 callsite 的parent_id在纯 SQL 中非常困难因此 Perfetto 标准库提供了现成的辅助模块INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree; SELECT -- 该调用栈中帧的函数名。 name, -- 包含该帧的 mapping 名称可以是原生二进制、库、JAR 或 APK。 mapping_name AS map_name, -- 该函数出现在调用栈任意位置时分配且*未释放*的内存量。 cumulative_size FROM android_heap_profile_summary_tree order by abs(cumulative_size) desc;示例输出节选namemap_namecumulative_size__start_thread/apex/com.android.runtime/lib64/bionic/libc.so392608_ZL15__pthread_startPv/apex/com.android.runtime/lib64/bionic/libc.so392608_ZN13thread_data_t10trampolineEPKS/system/lib64/libutils.so199496_ZN7android14AndroidRuntime15javaThreadShellEPv/system/lib64/libandroid_runtime.so199496_ZN7android6Thread11_threadLoopEPv/system/lib64/libutils.so199496_ZN3art6Thread14CreateCallbackEPv/apex/com.android.art/lib64/libart.so193112_ZN3art35InvokeVirtualOrInterface.../apex/com.android.art/lib64/libart.so193112_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc/apex/com.android.art/lib64/libart.so193112art_quick_invoke_stub/apex/com.android.art/lib64/libart.so193112该模块的实现位于 src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profile/summary_tree.sql它先从heap_profile_allocation按 callsite 聚合出self_size与self_alloc_size再利用graphs.scan模块沿parent_id向上展开调用树最终输出每个函数含祖先链上所有函数的累积分配量方便快速定位谁分配了这 392KB。同目录下的callstacks.sql、intervals.sql还提供了调用栈展开与时间窗口聚合等更多标准库能力。【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考