基本概念

如需了解 Android 上的内存用量,您必须从多个不同的角度(从高级 Java 对象到低级内核页面)来审视系统。

性能断崖

内存用量本身没有好坏之分,重要的是您将内存用于何处。不过,当您接近设备上的可用内存上限时,最终会“遭遇性能断崖”。

一张图表,显示在达到特定内存阈值之前性能稳定,随后随着系统开始抖动和终止进程,性能急剧下降

当您远离悬崖时,增加少量内存用量(例如 50MB)可能不会对性能产生明显影响。但是,当可用 RAM 耗尽时,就会达到临界点。此时,操作系统必须开始分页内存并终止进程以释放空间。超过这个临界点后,即使内存用量只是略微增加,也可能导致严重的“抖动”,即设备因关键进程被终止而无响应或似乎重启。

Android 堆栈

Android 堆栈的每一层都有其独特的内存视图:

Android 内存堆栈的框图,其中 Java 应用位于顶部,ART 位于其下方,Linux 内核和物理页面位于底部

  1. 应用 (Java/Kotlin):开发者主要会看到在 Java 堆上分配的 Java 对象。
  2. Android 运行时 (ART):ART 通过使用内核中的虚拟内存页面来管理 Java/Kotlin 堆。以前称为 Dalvik(这两个术语有时可互换使用)。
  3. Linux 内核:内核将内存视为物理和虚拟页面。传统上,这些页面大小为 4KB,但 Android 也支持 16KB 页面大小。内核对“Java 对象”一无所知。

如果您想优化内存,则必须在自己的层内进行优化(例如,加载更少的位图),或者了解您下方的层,以了解您的高级分配如何转换为物理页面使用情况。

内存类型:匿名内存与有文件支持的内存

在深入了解操作系统如何回收内存之前,您必须先了解 Linux 中内存页的两个基本类别:

  1. 匿名内存 (anon):没有 存储器中的文件支持的内存。这包括 C/C++ malloc(如 Scudo 分配器)分配的内存,以及在 Java/Kotlin 堆上为 Java 对象分配的内存。由于此内存没有可返回的源文件,因此操作系统必须将其保留在 RAM 中,或者在内存不足时将其压缩并换出到 ZRAM。
  2. 与文件对应的内存 (file):直接从存储设备上的文件映射的内存。这包括可执行代码(DEX 文件、.so 原生库)和字体。

内存映射文件 (mmap)

Android 使用 mmap 将文件支持内存和匿名内存映射到进程的地址空间。

在处理文件支持的内存时,页面会进一步分类为:

  • 干净内存:从文件映射且未经修改的页面。如果系统需要更多内存,内核可以直接丢弃这些页面,因为稍后可以直接从存储设备上的文件中重新加载这些页面。
  • 脏内存:已被进程修改的页面。这些页面无法丢弃;它们的行为类似于匿名内存,必须保留在 RAM 中或移至 ZRAM。

Android 倾向于使用易于进行内存映射的文件格式(如 DEX),以便系统在内存压力下轻松回收干净内存。由于匿名内存和脏内存无法简单地丢弃,因此私有脏内存/匿名内存过高是导致系统抖动和低内存终止守护程序 (LMK) 干预的主要原因。如果您的应用使用大量私有脏内存,则会直接导致性能断崖式下跌。

Zygote 进程模型

Android 通过使用名为 Zygote 的进程,最大限度地降低了启动新应用的成本。

  1. Zygote 在系统启动时启动,并将通用框架类和资源预加载到其内存中。从内核的角度来看,这会变成匿名脏内存,但它是 Zygote 进程独有的。
  2. 当新应用启动时,系统会分叉 Zygote 进程。
  3. 新的子进程使用具有写时复制 (COW) 语义的共享映射来继承 Zygote 的内存。

一张示意图,展示了 Zygote 进程使用写时复制语义与新派生的子进程共享内存页

只要子进程仅读取从 Zygote 继承的内存,物理内存页面就会在所有进程之间保持共享。当子进程修改共享页面时,内核会以透明方式为该进程创建该页面的私有副本。这种模型允许多个进程共享其大部分内存(尤其是框架代码和资源),从而显著减少系统的整体内存占用。

RSS、PSS、USS 和 memcg

由于内存会在进程之间大量共享(主要通过 Zygote 模型),因此有多种方法可以计算进程的内存用量。RSS、PSS 和 USS 会统计映射到进程地址空间的页面:

一张图表,直观展示了常驻内存大小 (RSS)、按比例分摊的内存大小 (PSS) 和独占内存大小 (USS) 之间的差异,并说明了共享内存页面的计算方式

  • RSS(常驻内存大小):进程在 RAM 中的总页数。这会高估用量,因为它会多次统计共享页面(每个共享页面的进程统计一次)。
  • PSS(按比例分摊的内存大小):进程独占的内存总量,加上其按比例分摊的共享内存。如果一个页面由 5 个进程共享,则每个进程会分摊该页面的 1/5。
  • USS(独占内存大小):进程独占的内存量。这是进程终止后会返回给系统的内存。

memcg:内核自己的统计

内核还会持续跟踪每个进程使用的内存总量,无论该内存是否已映射。在 Android 上,每个应用进程都属于其自己的内存控制组 (memcg),嵌套在整个应用的 memcg 中(请参阅 Android memcg 层次结构)。每当分配内存页面时,内核都会将其计入分配该页面的进程的 memcg。总费用位于 memcg 的 memory.current 文件中。

memcg 统计与 RSS、PSS 和 USS 存在一些重要差异:

  • 每个页面仅收费一次,费用由首次将其加载到内存中的进程的 memcg 承担。即使其他进程稍后映射同一页面,该费用也会保留。Zygote 预加载的页面会计入 Zygote,而不是您的应用。当您的应用写入其中一个页面时,私有副本会计入您的应用。
  • 它包含页表指标无法看到的内存。memory.current 会统计应用在未映射的情况下读取的文件页数(例如,使用 read 或 pread 读取的数据库页数)。
  • 它会单独跟踪交换。压缩到 ZRAM 中的内存会从 memory.current 移出并移入 memory.swap.current,因此您可以一目了然地看到这两者。
  • 读取成本低。计数器始终保持最新状态,读取计数器不会锁定或扫描应用的地址空间。
  • 这是系统限制。Android 应用于应用的内存限制和内存预算会作用于 memcg 费用。

显存(例如硬件位图背后的缓冲区)通常不会计入应用的 memcg。

何时使用哪些指标?

选择合适的内存指标取决于您要衡量的内容以及衡量环境。在搭载 Android 17 QPR2(API 级别 37.2)及更高版本的设备上,跟踪您自己的应用时,请从 memcg 开始。

指标 适用场景 密钥强度 主要缺点
memcg 跟踪单个应用在一段时间内的表现,并与预算进行比较 每个页面计数一次,包括未映射的页面缓存,单独跟踪交换。读取成本低。 在 Android 17 QPR2 之前,并非所有设备都支持此功能。共享页面会计入最先接触它们的进程。
PSS 系统级内存快照 准确分配共享内存。PSS 的总和等于总内存用量。 根据其他正在运行的进程而波动。不适合比较特定应用在一段时间内的表现。
RSS 长期跟踪单个应用,现场遥测 稳定,独立于其他进程。收集速度快且成本低。 由于多次计算共享内存,因此会高估总用量。
USS 评估终止进程的影响 确切显示如果应用终止,将释放多少内存。 完全忽略共享内存。

memcg(内存控制组费用)

  • 最适合:在搭载 Android 17 QPR2 或更高版本的设备上,跟踪单个应用在一段时间内、在关键用户历程 (CUJ) 期间或在不同应用版本之间的内存用量。在校准内存预算时,这也是需要关注的指标,因为它是预算限制的数量。
  • 原因:与 RSS 一样,当其他应用开始或停止与您的应用共享页面时,memcg 费用不会发生变化。与 RSS 不同,它绝不会对页面进行重复计数,因此您可以将应用的所有正在运行的进程的费用相加。整个应用的 memcg 可能会高于该总和,因为它会保留已退出进程的缓存文件页面,直到内核回收这些页面。它还会统计 RSS、PSS 和 USS 遗漏的内存,例如未映射的文件缓存。读取该文件的开销很低,因为内核会在对页面进行计费时更新计数器。
  • 读取方法:读取进程的 memcg 目录中的 memory.current、memory.swap.current 和 memory.stat(请参阅 memcg 统计信息)。在 Android 18 及更高版本中,dumpsys meminfo 也会输出这些信息。在应用内,使用 MemoryBudgetManager。
  • 现场测量最佳实践:出于 RSS 中所述的相同原因,请跟踪匿名内存 + 交换空间:memory.stat 的 anon 字段加上 memory.swap.current。memory.stat 的 file 字段包含应用读取过一次且内核尚未回收的文件页,因此在应用停止使用该内存后,memory.current 可能会在一段时间内保持较高水平。
  • 何时需要注意:由于页面会归入最先访问它们的进程,因此如果您的应用在任何其他进程之前读取共享文件页面(例如字体或共享库),则该页面会归入您的应用。 相反,另一个进程也可能会因您的应用也使用的页面而被收费。预计不同设备和运行之间的文件支持内存会有细微差异,并在比较时依赖 anon 和交换。

PSS(按比例分摊的内存大小)

  • 最适合:在给定时间获取特定设备的完整系统内存快照。
  • 原因:PSS 具有一个实用的数学属性,即所有正在运行的进程的 PSS 之和等于系统中的进程所使用的总内存。它能完美分配共享内存,而不会重复统计。
  • 何时应避免使用:您不得使用 PSS 来比较两个不同快照、不同时间或不同设备中的特定进程。由于 PSS 取决于当前有多少其他进程与您的进程共享页面,因此即使应用的实际行为完全保持不变,应用的 PSS 也可能会波动。

RSS(常驻内存大小)

  • 最适合:跟踪单个应用在不同关键用户历程 (CUJ) 中随时间变化的内存用量,或比较不同应用版本的内存用量。 这也是现场遥测最实用的选择。
  • 原因:RSS 比 PSS 更稳定。它会告知您映射到进程中的内存,而不管其他应用在做什么。此外,RSS 的收集成本较低。测量 PSS 或 USS 需要扫描复杂的内核内存管理结构并获取锁,如果收集频率过高,可能会导致系统性能问题(例如界面卡顿)。
  • 现场测量最佳实践:从实际设备收集数据时,通常最好测量匿名 RSS + 交换 (ZRAM)。
    • 为什么仅限匿名?由于系统级 pagecache 压力,内核可以随时逐出文件支持的页面(例如代码、应用资源、字体或其他内存映射文件)。在跟踪特定应用的指标中包含文件页会引入来自操作系统内存管理决策的干扰,这些决策可能是由其他应用或系统的内存压力驱动的。相反,匿名内存(例如 Java 堆和原生堆)由应用直接控制。
    • 为何要添加交换空间?在内存压力下,操作系统会压缩匿名页并将其移至 ZRAM(交换空间)。如果您仅测量常驻匿名内存,则指标可能会错误地显示内存用量“减少”,这仅仅是因为系统面临压力并换出了您的页面。添加交换内存可确保您将应用分配的所有匿名内存都纳入考量,无论这些内存当前位于 RAM 还是 ZRAM 中。
    • 尽可能使用 memcg。在搭载 Android 17 QPR2 及更高版本的设备上,memory.stat 中的 anon 加上 memory.swap.current 可提供类似的测量结果,该结果不包括与 Zygote 共享的匿名页面,并且收集成本更低(请参阅 memcg)。

USS(独占内存大小)

  • 适用场景:确定终止进程对系统的直接影响。
  • 原因:USS 准确表示如果进程终止(例如,被低内存终止守护程序 lmkd 终止),系统会立即回收并返还的确切内存量。
  • 使用方法:此指标在评估持久进程或后台服务时非常有价值。如果您要评估后台进程对整个系统的开销,USS 会准确告诉您系统仅为了让该特定进程保持活跃状态而牺牲了多少内存。

↑ 向上 | 工具 →