内存限制器

Android 17 及更高版本包含内存限制器,这是一种系统服务,可使用 Linux cgroup v2 监控和限制应用进程的内存用量。内存限制器可防止单个应用消耗过多的系统内存,从而减轻整体内存压力,并防止关键前台进程和缓存进程因内存不足而被强制终止。

机制

内存限制器与 Activity Manager Service (AMS) 集成,可跟踪进程生命周期事件和状态变化。内存限制器使用 Linux 内核 cgroup v2 层次结构强制执行内存限制。

为了支持内存限制器,设备内核必须启用 cgroup v2 和 memory 控制器。该服务特别依赖于以下属性:

memory.high
应用于软件包 UID cgroup 的软回收阈值,或在每个进程的内存预算处于活跃状态时应用于进程 PID cgroup 的软回收阈值。默认为平台内存限制的 90%,除非受到内存预算的限制。如果超出此限制,内核会限制分配并从该 cgroup 回收页面。
memory.max
在平台内存限制的 100% 处应用于软件包 UID cgroup 的硬内存上限。如果回收无法将内存用量保持在 memory.max 以下,内核会终止内存不足的进程。
memory.swap.max
限制每个进程 PID cgroup 可以使用的最大交换空间(例如 ZRAM)。

Cgroup 和内核要求

搭载 Android 17(API 级别 37)及更高版本的设备必须满足以下 cgroup v2 和内核要求,才能实现平台限制,以及 API 级别 37.2 及更高级别中的清单预算和 MemoryBudgetManager:

  • 按进程激活 cgroup:在 /system/etc/cgroups.json 中将 "NeedsActivation": true 保留在 cgroup v2 memory 控制器上,以便早期 init 在创建 /sys/fs/cgroup/apps/uid_<UID>/pid_<PID> 和 /sys/fs/cgroup/system/uid_<UID>/pid_<PID> 之前激活 +memory。请勿停用 "NeedsActivation"、设置 ro.config.per_app_memcg=false 或添加停用每个 UID 或每个进程的内存 cgroup 的供应商 cgroups.json 替换。如果 /vendor/etc/task_profiles.json 替换了内存控制器属性(例如 MemLimit 或 MemSoftLimit),请使 "File" 保持非空状态,并添加 cgroup v2 文件名的 "FileV2" 映射。

  • Cgroup 文件权限:/sys/fs/cgroup/apps 和 /sys/fs/cgroup/system 下的父级 uid_<UID> 以及每个进程的 pid_<PID> 控制文件 memory.high、memory.max 和 memory.swap.max 必须可由 UID 或 GID 1000 下的 system_server 写入,以便在没有 EACCES 错误的情况下应用 UID 限制、进程预算和交换限制。每个 UID 和每个进程的 memory.current、memory.events、memory.events.local、memory.stat 和 memory.high 文件,以及每个 UID 的 memory.max 和 memory.swap.max,都必须可由所属应用 UID 读取,以便 MemoryBudgetManager 查询和超预算监听器在进程内运行。

  • 本地 PSI 隔离:通过添加 GKI 本地 memcg PSI 更改并在任何 android_vh_mem_cgroup_handle_over_high 供应商钩子中保留 record_psi=false,将每个 cgroup 的 memory.high 回收失速与全局内存压力失速信息 (PSI) 隔离开来。如果没有 PSI 隔离,局部 memory.high 限制会增加全局内存 PSI,并可能在系统级内存没有压力时触发不必要的 lmkd 进程终止。

对应用的影响

在内存限制范围内运行的应用不受内存限制器的影响。

当应用超出其 memory.high 限制时,内核会逐出应用的干净文件支持内存,并换出其非活跃匿名内存,以使应用保持在限制范围内。由于这种页面逐出和交换活动,应用可能会遇到暂时的执行限制,但会继续运行。

在极端情况下,例如失控的内存泄漏(应用在超过 memory.max 或交换空间耗尽后继续分配内存),应用无法分配内存并终止。

Process Monitoring

内存限制器会将本页面上的平台限制应用于应用进程 (UID >= 10000)。UID 低于 10000 的进程不会受到平台限制,默认设备端沙盒化推理服务 (config_defaultOnDeviceSandboxedInferenceService) 的软件包也不会受到限制。系统应用和服务仍然可以声明更严格的内存预算,如系统内存预算中所述。

内存限制器会根据进程的可见性状态分配内存限制:

  • 可见进程:托管用户可见界面的进程,例如获得焦点的 activity 或活跃互动。由于显示界面需要更大的工作集来运行渲染和图形流水线,因此可见进程会获得更宽松的内存限制。

  • 不可见进程:在后台执行工作而不显示界面的进程,例如后台服务或广播接收器。由于它们不渲染界面组件,因此在更严格的限制下运行。

下表将特定进程状态映射到平台内存限制分类:

进程状态内存上限
PERSISTENT 无限制
PERSISTENT_UI 无限制
TOP visible
BOUND_TOP visible
FOREGROUND_SERVICE not-visible
BOUND_FOREGROUND_SERVICE not-visible
IMPORTANT_FOREGROUND visible
IMPORTANT_BACKGROUND not-visible
TRANSIENT_BACKGROUND not-visible
BACKUP not-visible
SERVICE not-visible
RECEIVER not-visible
TOP_SLEEPING visible
HEAVY_WEIGHT not-visible
HOME not-visible
LAST_ACTIVITY not-visible
CACHED_ACTIVITY not-visible
CACHED_ACTIVITY_CLIENT not-visible
CACHED_RECENT not-visible
CACHED_EMPTY not-visible

PERSISTENT 和 PERSISTENT_UI 进程不会收到平台限制。如果其中一个进程声明了内存预算,则其前台预算适用,如内存预算存储桶中所述。

在缓存状态下,进程会保留 not-visible 平台限制,同时进行缓存应用冻结和压缩,即使未发生冻结或最大程度的回收,也能使进程保持在限制范围内。

当进程超出其分配的 memory.high 限制时,内存限制器会检测到该事件,并可触发诊断操作,例如捕获内存配置文件或将异常记录到 statsd。

内存预算存储桶

在 API 级别 37.2 及更高级别中,应用和系统服务可以声明比平台限制更严格的内存预算,也可以通过 MemoryBudgetManager 设置更严格的运行时预算。应用会按照设置应用内存预算中的说明配置预算。对于系统应用和服务,请参阅系统内存预算。预算会设置 memory.high,并且不能超过同一进程状态的平台限制,而运行时预算不能超过适用的清单预算。

当进程声明清单预算时,内存限制器会将其进程状态映射到三个预算存储桶之一,而运行时预算会应用单个上限,直到更新或清除。清单 android:state 值 foreground、perceptible 和 background 用于指定匹配存储桶的预算。处于 CACHED_ACTIVITY 到 CACHED_EMPTY 状态的缓存进程会保留 background 预算,同时会冻结和压缩缓存的应用。

进程状态预算范围
PERSISTENT前景
PERSISTENT_UI前景
TOP前景
BOUND_TOP前景
TOP_SLEEPING前景
FOREGROUND_SERVICE可感知
BOUND_FOREGROUND_SERVICE可感知
IMPORTANT_FOREGROUND可感知
IMPORTANT_BACKGROUND可感知
TRANSIENT_BACKGROUND背景
BACKUP背景
SERVICE背景
RECEIVER背景
HEAVY_WEIGHT背景
HOME背景
LAST_ACTIVITY背景
CACHED_ACTIVITY背景
CACHED_ACTIVITY_CLIENT背景
CACHED_RECENT背景
CACHED_EMPTY背景

配置

在 Android 17 及更高版本中,内存限制器通过系统分区上的标准平台配置文件进行配置,该文件位于 /system/etc/memory-limiter-config.xml。

XML 格式

配置文件遵循 memory-limiter-config.xsd 中定义的架构。该文件定义了按可用内存排序的多个限制集;服务会根据设备的可用 RAM 选择最高的匹配限制集。所有内存值均以兆比字节 (MiB) 为单位指定。

<MemoryLimiterConfig>
  <version>1</version>
  <configList>
    <!-- RAM boundaries account for MemTotal excluding carve-outs. -->
    <limitSet>
      <!-- Limits for 16GB RAM device [14336, 18432): 10G/5G/5G/5G -->
      <minimumRequiredMemTotal>14336</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>18432</maximumAllowedMemTotal>
      <memVisible>10240</memVisible>
      <memNotVisible>5120</memNotVisible>
      <swapVisible>5120</swapVisible>
      <swapNotVisible>5120</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 12GB RAM device [9216, 14336): 8G/4G/4G/4G -->
      <minimumRequiredMemTotal>9216</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>14336</maximumAllowedMemTotal>
      <memVisible>8192</memVisible>
      <memNotVisible>4096</memNotVisible>
      <swapVisible>4096</swapVisible>
      <swapNotVisible>4096</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 8GB RAM device [6800, 9216): 5G/3G/3G/3G -->
      <minimumRequiredMemTotal>6800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>9216</maximumAllowedMemTotal>
      <memVisible>5120</memVisible>
      <memNotVisible>3072</memNotVisible>
      <swapVisible>3072</swapVisible>
      <swapNotVisible>3072</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 6GB RAM device [4800, 6800): 4G/2G/2G/2G -->
      <minimumRequiredMemTotal>4800</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>6800</maximumAllowedMemTotal>
      <memVisible>4096</memVisible>
      <memNotVisible>2048</memNotVisible>
      <swapVisible>2048</swapVisible>
      <swapNotVisible>2048</swapNotVisible>
    </limitSet>
    <limitSet>
      <!-- Limits for 4GB RAM device [3200, 4800): 2G/1G/1G/1G -->
      <minimumRequiredMemTotal>3200</minimumRequiredMemTotal>
      <maximumAllowedMemTotal>4800</maximumAllowedMemTotal>
      <memVisible>2048</memVisible>
      <memNotVisible>1024</memNotVisible>
      <swapVisible>1024</swapVisible>
      <swapNotVisible>1024</swapNotVisible>
    </limitSet>
  </configList>
</MemoryLimiterConfig>
version
一个正整数,用于标识配置版本。此设置必须是 1。
minimumRequiredMemTotal

应用此限制集所需的最低系统 MemTotal(以 MiB 为单位,包含下限)。该服务会根据内核在 /proc/meminfo 中报告的总可用内存来评估此值。

了解内存预留和边界:内核 MemTotal 值反映了在扣除硬件和固件内存预留后,操作系统可访问的物理 DRAM。预留内存会为 GPU、基带调制解调器、相机 ISP、安全执行环境和虚拟机监控程序等专用硬件组件预留 RAM,通常会消耗 500 MiB 到 2 GiB 以上的物理内存。

由于 MemTotal 低于设备标称的物理 DRAM,因此 minimumRequiredMemTotal 阈值包含这些预留内存的余量。例如,针对 16 GB 设备的配置指定下限为 14336 (14 GiB),而不是 16384 (16 GiB);针对 12 GB 设备的层级指定下限为 9216 (9 GiB)。这可确保每个设备都与其预期层级匹配,而不管板级 carve-out 变体如何。

maximumAllowedMemTotal

要应用此限制集的系统 MemTotal 上限(以 MiB 为单位,不含上限)。如果 MemTotal 大于或等于此上限,则设置的限制不适用。对于 MemTotal 小于 3,200 MiB 或至少为 18,432 MiB 的设备,内存限制器不会应用默认平台限制,但每个进程的内存 cgroup 仍处于活跃状态(以及 API 级别 37.2 及更高级别中的清单、系统和 MemoryBudgetManager 运行时预算)。

memVisible

当软件包具有可见进程时,应用于软件包 UID cgroup 的 memory.max 内存硬限制(以 MiB 为单位)。默认的 memory.high 软回收阈值是此值的 90%,四舍五入到最接近的 MiB。

memNotVisible

当软件包没有可见进程时,应用于软件包 UID cgroup 的 memory.max 内存硬限制(以 MiB 为单位)。默认 memory.high 软回收阈值是此值的 90%,四舍五入到最接近的 MiB。

swapVisible

应用于每个可见进程 PID cgroup 的 memory.swap.max 交换空间限制(以 MiB 为单位)。

swapNotVisible

应用于每个不可见进程 PID cgroup 的 memory.swap.max 交换空间限制(以 MiB 为单位)。

标准平台限制

在 Android 17 及更高版本中,平台使用 [floor, ceiling) 范围间隔在 /system/etc/memory-limiter-config.xml 中指定常见物理 RAM 层级的标准内存和交换限制:

目标物理 RAM MemTotal 范围 [下限,上限)
(minimumRequiredMemTotal,
maximumAllowedMemTotal)
可见内存上限
(memVisible)
不可见内存上限
(memNotVisible)
可见交换限制
(swapVisible)
不显示交换限制
(swapNotVisible)
16 GB [14336, 18432) MiB 10,240 MiB (10 GiB) 5,120 MiB (5 GiB) 5,120 MiB (5 GiB) 5,120 MiB (5 GiB)
12 GB [9216, 14336) MiB 8,192 MiB (8 GiB) 4,096 MiB (4 GiB) 4,096 MiB (4 GiB) 4,096 MiB (4 GiB)
8 GB [6800, 9216) MiB 5,120 MiB (5 GiB) 3,072 MiB (3 GiB) 3,072 MiB (3 GiB) 3,072 MiB (3 GiB)
6 GB [4800, 6800) MiB 4,096 MiB (4 GiB) 2,048 MiB (2 GiB) 2,048 MiB (2 GiB) 2,048 MiB (2 GiB)
4 GB [3200, 4800) MiB 2,048 MiB (2 GiB) 1,024 MiB (1 GiB) 1,024 MiB (1 GiB) 1,024 MiB (1 GiB)

内存限制原则

内存限制器配置是围绕以下平台原则设计的:

  • 生态系统一致性和应用兼容性:设备间一致的内存限制可确保整个 Android 生态系统中的应用性能可预测。应用是根据标准内存预期进行开发和测试的;保持统一的平台限制可避免意外限制或过早终止。

  • 按比例分配资源:平台限制根据物理 RAM 容量进行校准:

    • 可见进程:分配了大约 1/2 到 2/3 的总物理 RAM,以支持活跃的界面、合成和渲染工作负载。
    • 不可见进程:为后台任务分配了大约 1/4 到 1/3 的总物理 RAM。
  • 普遍适用性:限制统一适用于设备上的所有应用进程 (UID >= 10000),包括预安装的系统应用和 OEM 应用。除了默认的设备端沙盒化推理服务外,内存限制器不会豁免个别应用,从而确保所有应用都受到公平的内存管理。如需了解如何为关键交互式守护程序配置自调优 memory.min 保护下限,以及如何为各个系统应用和服务配置更严格、根据工作负载校准的 memory.high 上限,请参阅系统内存预算。

  • 可预测的运行时预算:在 API 级别 37.2 及更高级别中,应用可以通过 MemoryBudgetManager 查询其内存限制和 cgroup 内存用量,并设置更严格的运行时预算。保留标准平台和清单限制,不进行供应商替换,可使运行时预算行为在不同设备上保持一致。

  • 内核回收和交换动态:当应用进程接近其 memory.high 限制时,Linux 内核会专门针对该进程 cgroup 进行内存回收。这包括丢弃不活跃的文件支持页面,以及将冷匿名页面交换到 ZRAM,从而在不施加全局内存压力的情况下保持系统流畅性。

Shell 命令

借助 am memory-limiter 命令,开发者和系统集成商可以在运行时与服务进行交互,以进行开发、测试和调试:

am memory-limiter <SUB-COMMAND>

状态

status 子命令会报告内存限制器的运行状态、配置的平台限制和活跃监控计数器:

adb shell am memory-limiter status

运行 adb shell am memory-limiter status version 会在启用双阈值内存限制器时输出 2,在停用时输出 0。

以下是 8 GB 设备上的 status 输出示例:

Memory limiter
  enabled           limits=true       killing=true      ignored=none
  visible:          memHigh=4608MB    memMax=5120MB     swapMax=3072MB
  not-visible:      memHigh=2765MB    memMax=3072MB     swapMax=3072MB
  active=64         uid-started=32    uid-current=28    uid-updated=4
  started=72        watched=72        watch-failed=0
  limiters=64       limiters-hwm=70   red-poll=0        red-limiter=0
  events=0          false-events=0    modify=84

输出中的关键字段包括:

limits和killing
指明是否针对设备的 RAM 层级配置并强制执行 memory.high 和 memory.max 平台内存限制。
ignored
被 ignore 子命令排除的 UID、all(当限制全局暂停时)或 none。
visible和not-visible
针对每个可见性分类强制执行的默认 UID memHigh 限制(针对 memory.high)、UID memMax 限制(针对 memory.max)和每个进程的 swapMax 限制(针对 memory.swap.max)。
active
system_server 中跟踪的进程和 UID 限制器的数量。
uid-started、uid-current和uid-updated
使用清单预算更新的 UID 限制器的累计启动计数、活跃计数和累计替换计数。
watched和watch-failed
已注册和失败的 cgroup memory.events 监视的累计数量。
limiters和limiters-hwm
活跃的原生 cgroup 观察器的数量和高水位线。
events和false-events
分派到 system_server 的超限事件数量,以及未超过受监控阈值的 memory.events 通知数量。

ignore

ignore 子命令会暂时将特定 UID 或所有进程从内存限制中排除。这对于性能基准测试、压力测试或诊断内存行为非常有用:

# Ignore a specific UID
adb shell am memory-limiter ignore 10087
# Ignore all processes (temporarily disables limiting)
adb shell am memory-limiter ignore all
# Resume normal limiting operation
adb shell am memory-limiter ignore none

手动

manual 子命令会使用自定义整数上限(最高可达目标的配置限制)覆盖计算出的 memory.high、memory.max 和 memory.swap.max 限制。传递 PID(无论是否带有 -p)以定位单个进程,或传递 -u <UID> 以定位软件包 UID 中的所有进程。以字节为单位指定大小,附加 MB 表示 MiB,附加 GB 表示 GiB,或传递 none 以清除替换。为了向后兼容,小于 20 且没有后缀的裸整数会被解释为 MiB:

# Set a 1 GiB limit for PID 1234, with or without -p
adb shell am memory-limiter manual 1234 1073741824
adb shell am memory-limiter manual 1234 1024MB
adb shell am memory-limiter manual -p 1234 1GB
# Set a 1 GiB limit for all processes in UID 10087
adb shell am memory-limiter manual -u 10087 1GB
# Remove the manual override for PID 1234 or UID 10087
adb shell am memory-limiter manual 1234 none
adb shell am memory-limiter manual -u 10087 none

手动进程替换仅在特定进程实例的生命周期内有效。如果进程重启,则会根据其状态恢复到标准限制。相比之下,manual -u <UID> 替换项会在 UID 保持活跃状态期间一直保留,直到 UID 中的所有进程都退出或您使用 none 清除替换项。

手动替换值不能超过物理系统限制。