ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Perfetto Android 内存分析实战指南:从 dumpsys meminfo 到 Native/Java 堆剖析

Perfetto Android 内存分析实战指南:从 dumpsys meminfo 到 Native/Java 堆剖析 Perfetto Android 内存分析实战指南从 dumpsys meminfo 到 Native/Java 堆剖析【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto本文基于 Perfetto 官方的内存案例研究文档docs/case-studies/memory.md系统讲解在 Android 上排查内存问题的完整工作流用dumpsys meminfo拿到内存快照、理解 Linux 内存管理底层概念PSS/RSS/脏页/交换页、用 Perfetto 追踪内存随时间的变化与低内存杀进程LMK、并用 heapprofd 与 ART 堆转储定位 native 与 Java 堆中的内存泄漏。读完后你可以独立复刻文档中的全部抓取配置并结合 Perfetto 源码理解这些计数器与剖析数据的实现原理。环境准备开始之前需要一台运行 macOS 或 Linux 的宿主机已安装并加入 PATH 的 ADB一台运行 Android 11 的设备。如果你要剖析自己的应用且设备不是 userdebug 版本需要在 AndroidManifest 中将应用标记为 profileable 或 debuggable。哪些应用可以被 heapprofd 选中详见 native heap profiler 文档。第一步用 dumpsys meminfo 获取内存快照调查某个进程内存使用情况的起点是adb shell dumpsys meminfo 包名它能给出进程各类内存占用的高层概览$ adb shell dumpsys meminfo com.android.systemui Applications Memory Usage (in Kilobytes): Uptime: 2030149 Realtime: 2030149 ** MEMINFO in pid 1974 [com.android.systemui] ** Pss Private Private SwapPss Rss Heap Heap Heap Total Dirty Clean Dirty Total Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 16840 16804 0 6764 19428 34024 25037 5553 Dalvik Heap 9110 9032 0 136 13164 36444 9111 27333 [more stuff...]关注Private Dirty列SystemUI 的 Java 堆Dalvik Heap占用约 9Mnative 堆Native Heap约 17M。为什么盯住的是 “Private Dirty”答案在下面的 Linux 内存管理基础中。Linux 内存管理基础clean、dirty、RSS、PSS 与 Swap 到底是什么从内核视角看内存被划分为大小相等的块称为页page通常是 4KiB。页被组织成虚拟地址上连续的区间称为VMAVirtual Memory Area。VMA 在进程通过mmap()系统调用向内核申请新的内存页池时创建。应用很少直接调用mmap()——native 进程中的调用通常由分配器malloc()/operator new()代劳Java 应用则由 Android Runtime 代理。VMA 分两种类型file-backed VMA文件在内存中的视图通过给mmap()传入文件描述符获得。内核通过该文件响应这个 VMA 上的缺页异常因此读取指向 VMA 的指针等价于对文件做read()。动态链接器ld在启动新进程或动态加载库时使用它Android 框架在加载新的 .dex 库或访问 APK 中的资源时也用。anonymous VMA纯粹内存区域背后没有任何文件。这是分配器向内核申请动态内存的方式通过mmap(... MAP_ANONYMOUS ...)获得。物理内存只在进程真正读写 VMA 时才以页为粒度分配。如果申请了 32 MiB 的页却只触碰了一个字节进程内存使用只增加 4KiB虚拟内存增加 32 MiB驻留物理内存只增加 4 KiB。优化程序内存用量时我们关心的是降低其在物理内存中的足迹。高虚拟内存占用在现代平台上通常不是问题除非地址空间耗尽这在 64 位系统上很难发生。进程驻留在物理内存中的内存量称为RSSResident Set Size。但并非所有驻留页都等价从内存消耗角度看VMA 内的单个页有以下状态Resident驻留页已映射到物理内存页。驻留页有两种状态Clean仅 file-backed 页页内容与磁盘一致。内存压力下内核更容易驱逐 clean 页因为再次需要时可以从底层文件重新读回。Dirty页内容已与磁盘分道扬镳或者多数情况下页根本没有磁盘支撑即 anonymous。dirty 页不能被驱逐否则数据丢失但可以被交换到磁盘或 ZRAM。Swapped已交换dirty 页可被写到 swap 文件多数 Linux 桌面发行版或压缩存储Android 与 CrOS 通过 ZRAM。页保持交换状态直到其虚拟地址上再次发生缺页内核才把它换回主存。Not present不存在页上从未发生过缺页或页曾是 clean 后来被驱逐。减少 dirty 内存通常比减少 clean 内存更重要dirty 内存不能像 clean 内存那样被回收在 Android 上即使被换入 ZRAM仍会占用一部分系统内存预算。这就是上面dumpsys meminfo示例中我们看Private Dirty的原因。共享内存可以映射进多个进程即不同进程中的 VMA 指向同一物理内存。这通常发生在常用库的 file-backed 内存上如 libc.so、framework.dex或者在进程fork()后子进程从父进程继承 dirty 内存时较少见。由此引出PSSProportional Set Size的概念在 PSS 中被多个进程驻留的内存按比例分摊给每个进程。如果把一个 4KiB 页映射进 4 个进程每个进程的 PSS 只增加 1KiB。小结动态分配的内存——无论来自 C 的malloc()、C 的operator new()还是 Java 的new X()——一开始总是anonymous且dirty除非从未被使用。如果这些内存长时间不读写或在内存压力下会被换出到 ZRAM 变成swapped。anonymous 内存无论resident即dirty还是swapped始终是资源消耗大户非必要应避免。file-mapped 内存来自代码Java 或 native、库和资源几乎总是clean。clean 内存同样侵蚀系统内存预算但应用开发者通常对它的掌控力较弱。观察内存随时间的变化dumpsys meminfo只能给出当前内存使用快照但即使极短的内存尖峰也可能导致低内存状况进而触发 LMK。文档给出两种调查手段RSS 高水位线High Watermark内存 tracepointRSS 高水位线/proc/[pid]/status文件包含大量信息其中就有内存信息。VmHWM展示进程自启动以来 RSS 的最大值由内核持续维护更新$ adb shell cat /proc/$(pidof com.android.systemui)/status [...] VmHWM: 256972 kB VmRSS: 195272 kB RssAnon: 30184 kB RssFile: 164420 kB RssShmem: 668 kB VmSwap: 43960 kB [...]源码印证Perfetto 的进程统计探针如何解析这些计数器上述/proc计数器正是 Perfettolinux.process_stats数据源轮询的对象。从源码结构看process_stats_data_source.cc 逐行解析/proc/pid/status对VmSize、VmLck、VmHWM、VmRSS、RssAnon、RssFile、RssShmem、VmSwap等键做字符串匹配只有值发生变化时才更新缓存cached.vm_hwm_kb、cached.rss_anon_kb等再与smaps_rollup中的Rss、Pss、Pss_Anon、Pss_File、Pss_Shmem、SwapPss合并写回最终在 UI 中形成mem.virt、mem.rss、mem.rss.anon、mem.rss.file、mem.swap、mem.rss.watermark等mem.*计数器轨道。这与 内存计数器文档 中给出的 SQL 查询示例一致select c.ts, c.value, t.name as counter_name, p.name as proc_name, p.pid from counter as c left join process_counter_track as t on c.track_id t.id left join process as p using (upid) where t.name like mem.%内存 tracepoints关于内存 tracepoint 的详细说明参见 数据源 内存 计数器与事件 页面。利用 Perfetto 可以从内核获取内存管理事件$ adb shell perfetto \ -c - --txt \ -o /data/misc/perfetto-traces/trace \ EOF buffers: { size_kb: 8960 fill_policy: DISCARD } buffers: { size_kb: 1280 fill_policy: DISCARD } data_sources: { config { name: linux.process_stats target_buffer: 1 process_stats_config { scan_all_processes_on_start: true } } } data_sources: { config { name: linux.ftrace ftrace_config { ftrace_events: mm_event/mm_event_record ftrace_events: kmem/rss_stat ftrace_events: kmem/ion_heap_grow ftrace_events: kmem/ion_heap_shrink } } } duration_ms: 30000 EOF配置要点双缓冲区8960 KiB 主缓冲DISCARD策略 1280 KiB 辅缓冲linux.process_stats通过target_buffer: 1写入辅缓冲scan_all_processes_on_start: true表示启动时扫描全部进程以建立线程/进程关联kmem/rss_stat是事件驱动push事件能捕获持续仅几毫秒的内存尖峰——这是/proc轮询做不到的详见 memory-counters.mdkmem/ion_heap_grow/kmem/ion_heap_shrink展示系统级 ION图形内存堆的使用变化仓库中这两个事件的 ftrace 解析有对应的测试数据位于 src/traced/probes/ftrace/test/data/synthetic/events/kmem 目录下。跟踪运行期间如果你跟着文档操作可以随手拍一张照片作为标记。结束后用adb pull /data/misc/perfetto-traces/trace ~/mem-trace拉取文件上传到 Perfetto UI。它会展示系统级 ION 使用统计和可展开的逐进程统计。向下滚动或 Ctrl-F找到com.google.android.GoogleCamera并展开即可看到相机各类内存指标的时序曲线可以看到在 trace 约 2/3 处mem.rss.anon轨道出现一次内存尖峰——那正是拍照的时刻。这是观察应用内存如何响应不同触发事件的好方法。选择正确的分析工具根据你想深挖的内存类型工具选择如下想深入Java 代码分配的 anonymous 内存dumpsys meminfo中标记为Dalvik Heap见 分析 Java 堆 一节。想深入native 代码分配的 anonymous 内存标记为Native Heap见 分析 Native 堆 一节。注意即使你的应用没有任何 C/C 代码也经常会有 native 内存——因为部分框架 API如正则表达式内部实现走的是 native 代码。想深入file-mapped 内存最佳选择是adb shell showmap PIDAndroid或直接查看/proc/PID/smaps。低内存杀进程LMK当 Android 设备内存不足时守护进程lmkd会开始杀进程以释放内存。各设备策略不同但一般按oom_score_adj分数降序杀进程即先杀后台应用与进程最后杀前台进程。Android 应用切换到后台时不会被杀掉而是保持cached缓存状态以便下次启动更快。这类应用的oom_score_adj较高通常最先被杀。用 Perfetto 可以收集 LMK 与oom_score_adj的信息$ adb shell perfetto \ -c - --txt \ -o /data/misc/perfetto-traces/trace \ EOF buffers: { size_kb: 8960 fill_policy: DISCARD } buffers: { size_kb: 1280 fill_policy: DISCARD } data_sources: { config { name: linux.process_stats target_buffer: 1 process_stats_config { scan_all_processes_on_start: true } } } data_sources: { config { name: linux.ftrace ftrace_config { ftrace_events: lowmemorykiller/lowmemory_kill ftrace_events: oom/oom_score_adj_update ftrace_events: ftrace/print atrace_apps: lmkd } } } duration_ms: 60000 EOF各事件的作用lowmemorykiller/lowmemory_kill捕获旧的内核态 lowmemorykiller 驱动的杀进程事件oom/oom_score_adj_update捕获进程 oom_score_adj 变化可由此推断进程状态前台、可见、后台、缓存atrace_apps: lmkd捕获用户态lmkd发出的 atrace 事件。用adb pull /data/misc/perfetto-traces/trace ~/oom-trace拉取后上传到 Perfetto UI可以看到相机应用打开时其 OOM score 降低被杀概率变小关闭后又升高。补充一点实现背景来自 memory-counters.mdAndroid 的 LMK 与标准 Linux OOM Killer 是完全不同的机制Perfetto 目前只支持 Android LMK 事件内核态与用户态都支持。在 SQL 层新旧两种 LMK 事件在导入时被归一化为instant表中mem.lmk键SELECT ts, process.name, process.pid FROM instant JOIN process_track ON instant.track_id process_track.id JOIN process USING (upid) WHERE instant.name mem.lmk同时应用状态可以从oom_score_adj区间推断缓存应用为 900–999前台应用为 0系统进程为 -900 等完整映射表见 内存计数器文档的 App states 一节。分析 Native 堆heapprofdNative Heap Profile 需要 Android 10。关于 native heap profiler 的详细指令与排障参见 数据源 堆剖析器 页面。应用通常通过malloc或 C 的new获取内存而不是直接向内核要。分配器保证内存被更高效地管理碎片较少并降低向内核要内存的开销。我们可以用heapprofd记录进程做的所有 native 分配与释放。生成的 profile 可以把内存使用归因到特定的函数调用栈上支持 native 与 Java 代码混合。注意profile 只显示其运行期间发生的分配运行前的分配不会出现在其中。抓取 profile使用仓库自带的 tools/heap_profile 脚本的android子命令来剖析连接设备上的进程$ tools/heap_profile android -n system_server Profiling active. Press CtrlC to terminate. You may disconnect your device. Wrote profiles to /tmp/profile-1283e247-2170-4f92-8181-683763e17445 (symlink /tmp/heap_profile-latest) The raw-trace and heap_dump.* (pprof) files can be visualized with https://ui.perfetto.dev.所有参数可通过tools/heap_profile android -h查看。看到Profiling active后操作一下手机结束后按 Ctrl-C。文档作者的示例中打开了几个应用。查看数据把输出目录中的raw-trace文件上传到 Perfetto UI点击出现的菱形标记。Measure 选择器中可用的指标Unreleased malloc sizedump 创建时刻该调用栈上已分配但尚未释放的字节数。Total malloc size该调用栈上累计分配的字节数包含 dump 时刻已释放的。Unreleased malloc count该调用栈上没有配对释放的分配次数。Total malloc count该调用栈上的总分配次数含已配对的。默认视图展示 profile 运行期间发生且未释放的分配Unreleased malloc size可以看到大量内存经由AssetManager.applyStyle的路径分配。在 Filters 框中输入 applyStyle 即可只看包含匹配帧的调用栈得到该路径的总分配量。由此就能明确代码中要查看的位置从而判断那块内存的用途以及是否真的需要全部。从源码结构看这些火焰图视图由 Trace Processor 的 stdlib SQL 模块生成位于 src/trace_processor/perfetto_sql/stdlib/android/memory/heap_profilecallstacks.sql、intervals.sql、summary_tree.sql其中 “Unreleased” 语义对应分配区间计算中的 retained 量。剖析裸 mmap 调用大多数 native 内存分配走malloc上面的 heapprofd 能看见。但某些组件如 ART、图形驱动、自定义分配器会用mmap直接向内核要内存——这类分配对 heapprofd不可见。调试它们可以利用一个事实与malloc相比mmap调用相对稀少。CPU profiling 里我们高频采样以降低开销而对mmap我们可以记录每一个事件而不产生显著性能影响。可以用两个数据源组合实现linux.ftracesyscall_events给出每次 mmap 调用的时间戳、参数size、flags与返回值。需要 Android 14 (U) 或更新版本。linux.perf配置 perf 采样器在mmap系统调用上触发关键在于设置period: 1即捕获每一次发生的调用栈。支持 Android 12 (S) 或更新版本。使用 Perfetto可以在单个 Perfetto 配置中组合两个数据源。注意mmap的 syscall ID 随架构不同arm64222下方示例使用x86_649buffers: { size_kb: 63488 fill_policy: RING_BUFFER } data_sources: { config { name: linux.ftrace ftrace_config { # 使用 syscall 名称Perfetto 负责 ID 映射。 # 需要 Android 14 syscall_events: sys_mmap syscall_events: sys_munmap syscall_events: sys_madvise # 可选捕获调度以查看是哪个线程在调用 mmap ftrace_events: sched/sched_switch } } } data_sources { config { name: linux.perf perf_event_config { timebase { period: 1 # 捕获每一次发生不做采样 tracepoint { name: raw_syscalls:sys_enter # FILTER: 222 是 arm64 上的 mmapx86_64 用 9。 filter: id 222 } } callstack_sampling { # 可选限定到特定目标 # scope { target_cmdline: your.app.package } kernel_frames: true } } } } duration_ms: 10000使用 Simpleperf如果只需要调用栈不需要 ftrace 参数时间线也可以用simpleperf的--tp-filter标志达到同样效果# 记录每次 mmaparm64 上 id 222的调用栈 adb shell simpleperf record -e raw_syscalls:sys_enter --tp-filter id 222 -a --duration 10 -g分析 Java 堆ART 堆转储ART 堆转储需要 Android 11。关于抓取 ART 堆转储的详细指令与排障参见 数据源 ART 堆转储 页面。转储 Java 堆可以获取构成 Java 堆的全部 Java 对象图的快照。使用仓库自带的 tools/java_heap_dump 脚本$ tools/java_heap_dump -n com.android.systemui Dumping Java Heap. Wrote profile to /tmp/tmpup3QrQprofile This can be viewed using https://ui.perfetto.dev.从脚本源码看除了-n 进程名外它还支持按 PID 指定目标-p、指定输出文件名-o、设置存放整个 Java 对象图的内存缓冲-b单位支持 kb/mb/gb、以固定毫秒间隔做连续转储-i0 表示禁用、抓取目标进程的/proc/$PID/smaps信息--smaps、--print-config调试模式以及等待OutOfMemoryError被抛出后再触发转储并可设置等待秒数等能力覆盖 本地 Android 抓取文档中 OOM 堆转储 所述的场景。查看数据上传 trace 到 Perfetto UI点击出现的菱形标记会呈现下面描述的火焰图视图。“Object Size” 与 “Object Count” 标签页这两个视图展示的是对象到 GC 根的最短路径上归属的内存。通常一个对象可达路径很多只展示最短的那条——这降低了展示数据的复杂度且通常是信噪比最高的路径。最右侧的(merged)栈是所有小到无法单独展示的对象之和。Object Size经由这条到 GC 根的路径保留了多少字节。Object Count经由这条路径保留了多少个对象。只想看包含某个字符串的帧时使用 Filters 框。例如想查所有与通知相关的分配就在 Filters 框输入 notification。与 native 堆 profile 一样可以通过类名聚焦图的某一方面上图中可以看到最左边的栈按类名聚合了路径。“Dominated Object Size” 与 “Dominated Object Count” 标签页把堆图呈现为火焰图树的另一种方式是展示它的dominator tree支配树。在堆图中若对象b从根出发只有经过a的路径才可达则对象a支配对象b。一个对象的支配者集合构成一条从根到该对象的链该对象被这条链上的所有对象独占保留。图中所有可达对象的这些链构成一棵树即 dominator tree。树的路径按类名聚合每个元素树节点代表一组类名相同、在 dominator tree 中位置相同的对象Dominated Object Size某节点中的对象独占保留了多少字节。Dominated Object Count某节点中的对象独占保留了多少个对象。从源码结构看这些视图由 stdlib 的 heap_graph SQL 模块 实现其中 dominator_tree.sql 与 raw_dominator_tree.sql 分别负责按类聚合与原始支配树的计算。工作流小结目标工具关键配置/命令版本要求内存快照dumpsys meminfo看 Private Dirty 列无RSS 高水位/proc/[pid]/statusVmHWM字段无内存随时间变化Perfettokmem/rss_statlinux.process_stats上文 30s trace 配置rss_stat 需较新内核LMK 与 oom_score_adjPerfettolowmemorykiller/lowmemory_killatrace_apps: lmkd上文 60s trace 配置无Java 堆匿名内存ART 堆转储 dominator treetools/java_heap_dump -n 包名Android 11Native 堆匿名内存heapprofdtools/heap_profile android -n 进程Android 10裸 mmap 分配ftrace syscall_events / linux.perfperiod: 1上文组合配置arm64 过滤id 222syscall_events 需 Android 14perf 方式需 Android 12file-mapped 内存showmap/smapsadb shell showmap PID无完整命令与配置可直接复现原文档 docs/case-studies/memory.md各数据源的计数器语义、SQL 查询与 TraceConfig 参考 内存计数器与事件文档heap profiler 与 ART 堆转储的排障细节分别见 native-heap-profiler.md 与 java-heap-profiler.md。【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表