Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存。这意味着应用修改的任何内存,无论修改的方式是分配新对象还是轻触内存映射的页面,都会一直驻留在 RAM 中,并且无法换出。若要从应用中释放内存,只能释放应用保留的对象引用,使内存可供垃圾回收器回收。这种情况有一个例外:对于任何未经修改的内存映射文件(如代码),如果系统想要在其他位置使用其内存,可将其从 RAM 中换出。
本页介绍了 Android 如何管理应用进程和内存分配。如需详细了解如何在应用中更高效地管理内存,请参阅管理应用内存。
垃圾回收
ART 或 Dalvik 虚拟机之类的受管内存环境会跟踪每次内存分配。一旦确定程序不再使用某块内存,它就会将该内存重新释放到堆中,无需程序员进行任何干预。这种回收受管内存环境中的未使用内存的机制称为垃圾回收。垃圾回收有两个目标:在程序中查找将来无法访问的数据对象,并回收这些对象使用的资源。
Android 的内存堆是分代的,这意味着它会根据分配对象的预期寿命和大小跟踪不同的分配存储分区。例如,最近分配的对象属于新生代。当某个对象保持活动状态达足够长的时间时,可将其提升为较老代,然后是永久代。
堆的每一代对相应对象可占用的内存量都有其自身的专用上限。每当一代开始填满时,系统便会执行垃圾回收事件以释放内存。垃圾回收的持续时间取决于它回收的是哪一代对象以及每一代有多少个活动对象。
尽管垃圾回收速度非常快,但仍会影响应用的性能。通常情况下,您无法从代码中控制何时发生垃圾回收事件。系统有一套专门确定何时执行垃圾回收的标准。当条件满足时,系统会停止执行进程并开始垃圾回收。如果在动画或音乐播放等密集型处理循环过程中发生垃圾回收,可能会增加处理时间,进而可能会导致应用中的代码执行超出建议的 16 毫秒阈值,无法实现高效、流畅的帧渲染。
此外,您的代码流执行的各种工作可能迫使垃圾回收事件发生得更频繁或导致其持续时间超过正常范围。例如,如果您在 Alpha 混合动画的每一帧期间,在 for 循环的最内层分配多个对象,可能会使内存堆受到大量对象的影响。在这种情况下,垃圾回收器会执行多个垃圾回收事件,并可能降低应用的性能。
如需详细了解有关垃圾回收的一般信息,请参阅垃圾回收。
共享内存
为了在 RAM 中容纳所需的一切,Android 会尝试跨进程共享 RAM 页面。它可以通过以下方式实现这一点:
- 每个应用进程都从一个名为 Zygote 的现有进程分叉。系统启动并加载通用框架代码和资源(如 activity 主题)时,Zygote 进程随之启动。为启动新的应用进程,系统会分叉 Zygote 进程,然后在新进程中加载并运行应用代码。这种方法使为框架代码和资源分配的大多数 RAM 页面可在所有应用进程之间共享。
- 大多数静态数据会内存映射到一个进程中。这种方法使得数据不仅可以在进程之间共享,还可以在需要时换出。静态数据示例包括:Dalvik 代码(通过将其放入预先链接的
.odex
文件中进行直接内存映射)、应用资源(通过将资源表格设计为可内存映射的结构以及通过对齐 APK 的 zip 条目)和传统项目元素(如.so
文件中的原生代码)。 - 在很多地方,Android 使用明确分配的共享内存区域(通过 ashmem 或 gralloc)在进程间共享同一动态 RAM。例如,窗口 surface 使用在应用和屏幕合成器之间共享的内存,而光标缓冲区使用在 content provider 和客户端之间共享的内存。
由于共享内存的广泛使用,在确定应用使用的内存量时需要小心谨慎。有关正确确定应用内存使用量的技巧,请参阅调查 RAM 使用量。
分配与回收应用内存
Dalvik 堆局限于每个应用进程的单个虚拟内存范围。这定义了逻辑堆大小,该大小可以根据需要增长,但不能超过系统为每个应用定义的上限。
堆的逻辑大小与堆使用的物理内存量不同。在检查应用堆时,Android 会计算按比例分摊的内存大小 (PSS) 值,该值同时考虑与其他进程共享的脏页和干净页,但其数量与共享该 RAM 的应用数量成正比。此 (PSS) 总量是系统认为的物理内存占用量。如需详细了解 PSS,请参阅调查 RAM 使用量指南。
Dalvik 堆不压缩堆的逻辑大小,这意味着 Android 不会对堆进行碎片整理来缩减空间。只有当堆末尾存在未使用的空间时,Android 才能缩减逻辑堆大小。但是,系统仍然可以减少堆使用的物理内存。垃圾回收之后,Dalvik 遍历堆并查找未使用的页面,然后使用 madvise 将这些页面返回给内核。因此,大数据块的配对分配和解除分配应该让使用的所有(或几乎所有)物理内存被回收。但是,从较小分配量中回收内存的效率要低得多,因为用于较小分配量的页面可能仍在与其他尚未释放的数据块共享。
限制应用内存
为了维持多任务环境的正常运行,Android 会为每个应用的堆大小设置硬性上限。不同设备的确切堆大小上限取决于设备的总体可用 RAM 大小。如果您的应用在达到堆容量上限后尝试分配更多内存,则可能会收到 OutOfMemoryError
。
在某些情况下,例如,为了确定在缓存中保存多少数据比较安全,您可能需要查询系统以确定当前设备上确切可用的堆空间大小。您可以通过调用 getMemoryClass()
向系统查询此数值。此方法返回一个整数,表示应用堆的可用兆字节数。
切换应用
当用户在应用之间切换时,Android 会将非前台应用保留在缓存中。非前台应用就是指用户看不到或未运行前台服务(如音乐播放)的应用。例如,当用户首次启动某个应用时,系统会为其创建一个进程;但是当用户离开此应用时,该进程不会退出。系统会将该进程保留在缓存中。如果用户稍后返回该应用,系统会重复使用该进程,从而加快应用切换速度。
如果您的应用具有缓存的进程且保留了目前不需要的资源,那么即使用户未使用您的应用,它也会影响系统的整体性能。当系统资源(如内存)不足时,它将会终止缓存中的进程。系统还会考虑终止占用最多内存的进程以释放 RAM。
注意:当应用处于缓存中时,占用的内存越少,就越有可能不被终止并能够快速恢复。但是,系统也可能根据当下的需求不考虑缓存进程的资源使用情况而随时将其终止。
如需详细了解未在前台运行的进程如何缓存以及 Android 如何确定哪些进程可终止,请参阅进程和线程指南。
内存压力测试
虽然内存压力问题在高端设备上不太常见,但它们仍然会导致低 RAM 设备(如搭载 Android [Go 版本] 的设备)的用户遇到问题。请务必尝试重现这种内存压力环境,以便编写插桩测试来验证应用行为,并改善低内存设备的用户体验。
压力应用测试
压力应用测试 (stressapptest
) 是一种内存接口测试,有助于创建真实的高负载场景,以测试应用在内存和硬件方面存在的各种限制。由于能够定义时间和内存限制,因此通过该测试,您可以编写插桩,以便验证哪些现实情况会出现内存耗用量高的问题。例如,使用以下命令集推送数据文件系统中的静态库,使其可执行,并在 990 MB 的情况下运行压力测试 20 秒:
adb push stressapptest /data/local/tmp/
adb shell chmod 777 /data/local/tmp/stressapptest
adb shell /data/local/tmp/stressapptest -s 20 -M 990
如需详细了解该工具的安装方法、常见参数和错误处理信息,请参阅 stressapptest 文档。
stressapptest 上的观察结果
stressapptest
等工具可用于请求自由分配机制无法分配的超大内存。此类请求可能会引发各种提醒,您应该从开发端了解此类提醒。低可用内存可能引发的三种主要提醒包括:
- SIGABRT:对您的进程来说,这是一种严重的原生代码崩溃,原因在于请求分配的内存大小大于可用内存,而此时系统内存本就很紧张。
- SIGQUIT:在插桩测试检测到后,将生成核心内存转储并终止进程。
- TRIM_MEMORY_EVENTS:这些回调适用于 Android 4.1(API 级别 16)及更高版本,并为您的进程提供详细的内存提醒。