快速评估工具

当您需要调查内存用量时,请先使用高级评估工具来确定哪些类别的内存用量最大。

dumpsys meminfo

如需大致了解应用的内存用量,最常用的方法是使用 dumpsys meminfo。如需查看包含所有内存映射的详细细分,添加 -a 标志通常很有帮助:

adb shell dumpsys meminfo -a <package_name_or_pid>

以下是应用在面临一些内存压力时的示例输出(请注意 SwapPss 较高):

** MEMINFO in pid 4562 [com.android.memorylab] **
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap      394      268       64    17440     4264    29416    18128     8040
  Dalvik Heap      165        8        0      672     8196    40949    32757     8192
 Dalvik Other       47       28        0      604     1132
        Stack      112      108        4      312      120
...
        TOTAL    33197      580      788    21920   167776    70365    50885    16232

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:       12                          32608
         Native Heap:      268                           4264
                Code:      724                         125672
               Stack:      108                            120
            Graphics:        0                              0
       Private Other:      256
              System:    31829
             Unknown:                                    5112

           TOTAL PSS:    33197            TOTAL RSS:   167776       TOTAL SWAP PSS:    21920

 Objects
               Views:       15         ViewRootImpl:        1
         AppContexts:        5           Activities:        1
...

解读数据

  • Pss Total:实际使用的物理内存(比例分配共享库占用的内存)。这是 Private Dirty 的总和
    • Private Clean + 您的进程与其他进程(例如 Zygote 启动映像)共享的内存的公平份额。这是衡量“此应用占用了多少内存”的最佳指标。
  • 私有脏:这是仅由您的进程使用且已被修改(或为匿名内存)的 RAM。这是查找泄漏的最关键数字,因为此内存无法丢弃。
  • 私有干净:只有您的进程使用的 RAM,但它是存储空间中文件的未修改副本(例如 DEX 文件)。如果内存不足,操作系统可能会将其丢弃。
  • SwapPss:已压缩并交换到 ZRAM 的内存。这可能是因为系统面临内存压力,或者 Android 运行时已识别出要压缩的后台进程(将其未使用或不活跃的页面移至 ZRAM)。在 dumpsys 示例中,几乎所有的 Native Heap 和 Dalvik Heap 都已移至 ZRAM。
  • 应用摘要:更高级别的分类,将详细行合并为易于理解的类别。
  • 对象:对于跟踪泄露的 Activities 和 ViewRootImpl 实例非常有用。如果您退出应用并强制执行 GC,Activities 计数应返回 0。

内存预算部分

在 Android 18 及更高版本中,dumpsys meminfo 在转储单个应用时还会输出 Memory Budget 部分。此部分会读取进程和整个应用(软件包)的 memcg 统计信息,因此您无需自行查找 cgroup 文件。以下是 MemoryLab 示例应用在前台运行时的部分,其中显示了点按三次 Allocate Java Memory (10MB) 后的情况:

 Memory Budget
                        Process(KB)       Package(KB)
                        -----------       -----------
  Current Usage:              83820             87976
  Limit:                       none           4718592
  Limit Source:                none     MemoryLimiter
  Peak Usage:                 85136             89292
  Swap Used:                      0                 0

  Usage Breakdown
    Anonymous:                81700             81700
    File:                        96              4200
    File Mapped:                 16               616
    Shared Memory:               16                16
    Kernel:                    1580              1632
    Page Tables:                640               640
    Slab:                       439               458

  Pressure & Reclaim
    Throttle Events:              0                 0
    Refaults (Anon):              0                 0
    Refaults (File):              0                 0
  • 当前用量:向 memcg (memory.current) 收取的内存。这是内存预算限制的数量。在示例中,在三次 10MB Java 分配后,它增加了大约 36,700 KB,与 TOTAL PSS 一致。即使对于单进程应用,如果先前运行的缓存文件页面尚未回收,软件包总数也会高于进程总数。
  • 限制:当前适用的回收阈值 (memory.high),如果没有,则为 none。在示例中,软件包具有系统内存限制,而进程本身没有限制。
  • 限制来源:限制的来源。AndroidManifest 是在应用清单中声明的 <memory-budget>,MemoryBudgetManager 是应用在运行时设置的预算,MemoryLimiter 是系统内存限制,am memory-limiter 是使用 adb 设置的手动替换。
  • Peak Usage:自 memcg 创建以来的最高费用 (memory.peak)。在不支持此指标的内核上,此指标会显示 N/A。
  • Swap Used:压缩到 ZRAM 中的内存 (memory.swap.current)。
  • 用量细分:memory.stat 的主要字段,例如匿名内存和文件支持的内存。
  • Pressure & Reclaim:Throttle Events 统计 memcg 超过其 memory.high 限制的次数。重新缺页会统计换出或逐出后又再次需要的页面。如果您在使用应用时这些数字不断攀升,则说明该限制比应用的工作集更严格。

如果使用 -a 或 -v 标志,该部分还会列出 memory.stat 和 memory.events 中的每个原始计数器。

实操练习:使用 meminfo 快速分类

此练习展示了不同类型的分配在 meminfo 中的显示方式。

  1. 启动应用:

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. 检查基准内存:

    adb shell dumpsys meminfo -s com.android.memorylab
    

    (-s 标志可提供简明扼要的内存类别摘要)。

  3. 分配 Java 内存:在应用中点按分配 Java 内存 (10MB) 按钮 3 次。

  4. 观察变化:再次运行 meminfo -s。您应该会看到 Java 堆值增加大约 30,000 KB (30MB)。

  5. 分配原生内存:点按一次分配原生内存(1GB 不可压缩)按钮。

  6. 观察变化:再次运行 meminfo -s。Native Heap 值现在应该会大幅增加(约 1,000,000 KB)。

  7. 与 memcg 进行比较(Android 18 及更高版本):查看 meminfo -s 输出末尾的 Memory Budget 部分。每次分配后,当前用量都会随着总 PSS 的增加而增加,因为 Java 和原生分配都会计入应用的 memcg。

全局视图

如需查看整个系统的内存用量,请运行以下命令:

adb shell dumpsys meminfo

这会提供 RAM 使用情况摘要,包括总 RAM、可用 RAM 以及按 PSS 排序的进程列表。

关键系统指标

  • ZRAM:RAM 中的压缩交换空间。
  • DMA-BUF:图形和共享内存缓冲区。现代 Android 版本广泛使用 DMA-BUF 进行界面渲染。如果 DMA-BUF 总数非常高,请使用 /sys/kernel/debug/dma_buf/bufinfo 调查图形密集型进程。
  • 丢失的 RAM:内核或任何进程未考虑的内存(例如,硬件预留区域)。

memcg 统计信息

每个应用进程都有自己的内存控制组 (memcg),内核会持续统计分配给它的内存总量(如需了解这种统计方式与 RSS 和 PSS 的区别,请参阅 memcg)。读取这些文件开销很低,不会干扰您的应用,因此您可以在运行用户体验流程时每秒对它们进行采样。

从 /proc/<PID>/cgroup 的 0:: 行中查找进程的 memcg 路径,而不是自行构建路径,因为 cgroup 布局可能因设备而异。将每个设备命令作为单引号字符串传递给 adb shell,以便设备 shell 看到引号:

PID=$(adb shell pidof -s <package_name>)

CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")

adb shell "cat $CG/memory.current $CG/memory.swap.current"

adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

关键文件如下:

  • memory.current:向进程收取的内存费用(以字节为单位)。这是内存预算限制的数量。
  • memory.swap.current:压缩到 ZRAM 中的内存(以字节为单位)。
  • memory.stat:费用明细,大小以字节为单位。anon 是匿名内存,例如 Java 堆和原生堆。file 是文件支持的页面缓存,例如代码和资源。workingset_refault_anon 和 workingset_refault_file 会统计被换出或逐出后又再次需要的页面。重新故障计数上升意味着进程内存不足。
  • memory.events:内存限制事件的计数。high 统计进程因超出其 memory.high 限制而被限制的次数。

进程 memcg 的父级目录属于整个应用(其 UID)。 其文件包括应用的所有运行中进程的内存,以及应用先前已退出且尚未回收的进程留下的缓存文件页:

adb shell "cat $CG/../memory.current"

如果您收到权限错误,请运行 adb root,然后重试。在某些设备上,您只能以 root 身份读取这些文件。

如需通过 MemoryLab 示例应用查看这些文件的引导式演示,请参阅实操练习:探索 memcg。

showmap

如需更详细地了解进程的内存映射,请使用 showmap。此工具会从 /proc/<pid>/smaps 读取数据。

adb shell showmap <pid>

showmap 会列出进程中的每个内存映射。需要查找的关键映射包括:

  • [anon:scudo:primary] 或 [anon:libc_malloc]:原生堆。现代 Android 版本使用 Scudo 分配器。
  • *.art:Java 堆映像(例如启动映像)。
  • [anon:dalvik-main space]:主 Java 堆。
  • *.so:共享库。
  • *.{oat,dex,odex,vdex}:编译后的 DEX 代码。

您可以对输出进行排序,以找出占用私有内存最多的进程:

# Sort by private clean + private dirty memory
adb shell showmap <pid> | awk '{print $6+$7, $0}' | sort -rn | head -n 10

示例输出:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
2776    18176     5784     2823        0     3008      308     2468     4700     4700         0         0         0        0        0        0   47 [anon:scudo:primary]
560      952      876      657      316        0      560        0       12       12         0         0         0        0        0        0    8 /data/app/.../base.apk
448   393216      448      448        0        0        0      448        0        0         0         0         0        0        0        0    2 [anon:dalvik-main space]
...

实操练习:使用 showmap 进行深入探究

了解如何识别特定的内存映射以及如何区分干净内存和脏内存。

  1. Mmap 清理内存:在 MemoryLab 应用中点按一次 Mmap Clean Anon Memory (10MB) 按钮。
  2. 获取进程 ID:应用的界面会显示其 PID。您也可以使用以下命令 找到它:

    adb shell pidof com.android.memorylab
    
  3. 使用 showmap 进行检查:

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. 识别映射:您应该会看到一个标记为 [anon:memorylab-clean] 的匿名映射,其虚拟大小正好为 10240 KB。请注意,此映射的私有脏值可能为 0。这是“干净”的匿名内存,因为我们使用 mmap 预留了虚拟空间,但尚未向其写入数据(因此实际上并未消耗任何物理 RAM)。

  5. 原生堆比较:将此结果与 [anon:scudo:primary] 映射进行对比,如果您在 meminfo 练习的第 5 步之后运行了 ,这些映射本应出现在 showmap 中。由于该 1GB 原生分配实际上会写入每个页面,因此这些页面是“脏”的,并且会大量显示在 private dirty 列中。

  6. 专业提示(大型对象):请注意 meminfo 练习中的 Java 堆分配。由于它们是 10MB 字节数组,因此它们可能会在 showmap 中显示为 [anon:dalvik-free list large object space]。Android 运行时 (ART) 会将大于 12KB 的对象放入单独的大型对象空间 (LOS) 中,以避免主堆中出现碎片。

procstats

meminfo 提供的是某个时间点的快照,而 procstats 则通过定期对所有正在运行的应用的状态和内存使用情况进行采样来提供历史数据。这是用于长期内存运行状况监控的主要工具,也是 Android 设置中内存用量报告的数据来源。

# View stats for the last 3 hours
adb shell dumpsys procstats --hours 3

了解状态

procstats 的输出可能非常庞大。最关键的部分是每个进程状态旁边的括号中的指标:

Top: 100% (54MB-64MB-67MB / 49MB-58MB-61MB / 110MB-120MB-130MB over 4)

格式为: (处于该状态的时间百分比)(样本中的 minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss)

  • Pss(按比例分摊的内存大小):共享内存在进程之间按比例分配。
  • Uss(独占集大小):仅由此进程使用的内存(相当于 Private Clean + Private Dirty)。
  • Rss(常驻内存大小):使用的总物理内存,包括所有共享页面(往往会重复计算)。

重要意义:

  • 稳定的应用:最小值/平均值/最大值彼此接近。
  • 内存泄漏:最大值明显高于最小值,并且随着样本数量的增加,平均值会随着时间的推移而逐渐上升。
  • 用量出现峰值:最大值远高于平均值,表示存在瞬时繁重操作(例如图片处理),如果这些操作在错误的时间发生,可能会导致 OOM。

进程状态

procstats 会跟踪处于多种状态(通常缩写)的应用:

  • 持久:始终运行的关键系统进程。
  • 顶部:应用当前位于前台,且对用户可见。
  • ImpFg(重要前台):关键后台进程(如 IME)或具有前台服务的应用。
  • ImpBg(重要后台):系统认为重要的后台进程。
  • Fgs/BFgs:前台服务或绑定前台服务。
  • Service/ServRst:后台服务或当前正在重启的服务。
  • Cached / LastAct:在后台缓存以便快速重启的应用。系统可以安全地终止这些进程。

设备内存状态和运行时统计信息

procstats 还会跟踪整个设备在内存压力下花费的时间。找到运行时统计信息部分:

Run time Stats:
  SOff/Norm: +1h10m15s
   SOn/Norm: +15m20s
        Low: +2m10s
       Crit: +45s
  • SOff / SOn:屏幕关闭与屏幕开启。
  • Norm / Mod / Low / Crit:内存压力级别(正常、中等、低、严重)。

如果设备长时间处于 Low 或 Crit 状态,用户会感到设备运行缓慢,因为内核会积极回收内存,并且低内存终止守护进程 (LMK) 会开始终止后台进程。然后,您可以查看每个软件包的统计信息,了解哪些应用在设备处于这些压力状态时内存用量较高。


← 概念 | ↑ 向上 | Java →