进程间内存分配

Android 平台基于这样的理念:空闲内存是浪费的内存。它始终尝试使用所有可用的内存。例如,系统会在应用关闭后将其保留在内存中,以便用户可以快速切换回这些应用。因此,Android 设备通常运行时可用内存很少。内存管理对于在重要的系统进程和多个用户应用之间正确分配内存至关重要。

本页讨论 Android 为系统和用户应用分配内存的基本原理。它还解释了操作系统如何应对内存不足的情况。

内存类型

Android 设备包含三种不同类型的内存:RAM、zRAM 和存储。请注意,CPU 和 GPU 访问的是相同的 RAM。

Types of memory

图 1. 内存类型 - RAM、zRAM 和存储

  • RAM 是速度最快的内存类型,但通常大小有限。高端设备通常拥有最大的 RAM 容量。

  • zRAM 是 RAM 的一个分区,用于交换空间。所有内容在放入 zRAM 时都会被压缩,并在从 zRAM 复制出来时解压缩。RAM 的这部分会随着页面移入或移出 zRAM 而扩大或缩小。设备制造商可以设置最大尺寸。

  • 存储包含所有持久性数据,例如文件系统以及所有应用、库和平台的包含对象代码。存储的容量远远大于其他两种类型的内存。在 Android 上,存储不像在其他 Linux 实现中那样用于交换空间,因为频繁写入会导致这种内存磨损,缩短存储介质的使用寿命。

内存页面

RAM 被分成页面。通常每个页面为 4KB 的内存。

页面被认为是空闲的或已使用的。空闲页面是未使用的 RAM。已使用页面是系统正在积极使用的 RAM,并被分为以下类别

  • 缓存:由存储上的文件支持的内存(例如代码或内存映射文件)。缓存内存有两种类型
    • 私有:由一个进程拥有,不共享
      • 干净:存储上文件的未修改副本,可以通过 kswapd 删除以增加空闲内存
      • 脏:存储上文件的已修改副本;可以通过 kswapd 移动到 zRAM 或在 zRAM 中压缩,以增加空闲内存
    • 共享:由多个进程使用
      • 干净:存储上文件的未修改副本,可以通过 kswapd 删除以增加空闲内存
      • 脏数据: 存储设备上文件的修改副本; 允许将更改写入存储设备上的文件,以通过 kswapd 增加可用内存,或者显式地使用 msync()munmap()
  • 匿名: 内存**不**由存储设备上的文件支持(例如,由 mmap() 分配,并设置 MAP_ANONYMOUS 标志)。
    • 脏数据: 可以通过 kswapd 在 zRAM 中移动/压缩,以增加可用内存。

随着系统积极管理 RAM,空闲页和已用页的比例会随着时间的推移而变化。本节介绍的概念是管理低内存情况的关键。本文档的下一节将更详细地解释它们。

低内存管理

Android 有两种主要的机制来处理低内存情况:内核交换守护进程和低内存杀手。

内核交换守护进程

内核交换守护进程 (kswapd) 是 Linux 内核的一部分,它将已用内存转换为可用内存。当设备上的可用内存不足时,守护进程就会变得活跃。Linux 内核维护低和高可用内存阈值。当可用内存低于低阈值时,kswapd 就会开始回收内存。当可用内存达到高阈值时,kswapd 就会停止回收内存。

kswapd 可以通过删除清洁页来回收它们,因为它们由存储设备支持并且没有被修改。如果进程尝试访问已被删除的清洁页,系统会将该页从存储设备复制到 RAM。此操作被称为按需分页

Clean page backed by storage deleted

图 2. 由存储设备支持的清洁页,已删除

kswapd 可以将缓存的私有脏页和匿名脏页移动到 zRAM,在那里它们会被压缩。这样做可以释放 RAM 中的可用内存(空闲页)。如果进程尝试访问 zRAM 中的脏页,该页会被解压缩并移回 RAM。如果与压缩页关联的进程被杀死,则该页会被从 zRAM 中删除。

如果可用内存低于某个阈值,系统就会开始杀死进程。

Dirty page moved to zRAM and compressed

图 3. 脏页移动到 zRAM 并压缩

低内存杀手

很多时候,kswapd 无法为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用程序内存不足,应该减少其分配。如果这还不够,内核就会开始杀死进程以释放内存。它会使用低内存杀手 (LMK) 来执行此操作。

为了决定杀死哪个进程,LMK 使用称为 oom_adj_score 的“内存不足”分数来优先处理正在运行的进程。分数高的进程将首先被杀死。后台应用程序将首先被杀死,系统进程将最后被杀死。下表列出了从高到低的 LMK 得分类别。最高得分类别中的项目(第一行)将首先被杀死。

Android processes, high scores at the top

图 4. Android 进程,分数高的在顶部,分数低的在底部

以下是上面表格中各个类别的描述

  • 后台应用程序: 之前运行过但当前未处于活动状态的应用程序。LMK 将首先杀死后台应用程序,从 oom_adj_score 最高的那个开始。

  • 先前应用程序: 最近使用的后台应用程序。先前应用程序比后台应用程序具有更高的优先级(分数更低),因为用户更有可能切换到它,而不是切换到其中一个后台应用程序。

  • 主页应用程序: 这是启动器应用程序。杀死它会导致壁纸消失。

  • 服务: 服务由应用程序启动,可能包括同步或上传到云。

  • 可感知应用程序: 非前台应用程序,以某种方式对用户可见,例如运行显示小型 UI 的搜索进程或收听音乐。

  • 前台应用程序: 当前正在使用的应用程序。杀死前台应用程序看起来像应用程序崩溃,这可能表明设备出现了问题。

  • 持久(服务): 这些是设备的核心服务,例如电话和 Wi-Fi。

  • 系统: 系统进程。当这些进程被杀死时,手机可能看起来像是重新启动。

  • 原生: 系统使用的非常低级的进程(例如,kswapd)。

设备制造商可以更改 LMK 的行为。

计算内存占用

内核跟踪系统中的所有内存页。

Pages used by different processes

图 5. 不同进程使用的页面

当确定应用程序使用了多少内存时,系统必须考虑共享页。访问相同服务或库的应用程序将共享内存页。例如,Google Play 服务和游戏应用程序可能会共享位置服务。这使得难以确定多少内存属于服务本身,而多少内存属于每个应用程序。

Pages shared by two apps

图 6. 两个应用程序共享的页面(中间)

为了确定应用程序的内存占用,可以使用以下任何指标

  • 驻留集大小 (RSS): 应用程序使用的共享页和非共享页的数量
  • 比例集大小 (PSS): 应用程序使用的非共享页的数量,以及共享页的平均分配(例如,如果三个进程共享 3MB,则每个进程在 PSS 中获得 1MB)
  • 唯一集大小 (USS): 应用程序使用的非共享页的数量(不包括共享页)

PSS 对操作系统很有用,因为操作系统想知道所有进程使用了多少内存,因为页面不会被多次计算。PSS 计算时间很长,因为系统需要确定哪些页面是共享的,以及由多少个进程共享。RSS 不会区分共享页和非共享页(使其计算速度更快),并且更适合跟踪内存分配的变化。

其他资源