1.内存
内存(Memory)是计算机的重要部件,也称内存储器和主存储器,它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。只要计算机开始运行,操作系统就会把需要运算的数据从内存调到CPU中进行运算,当运算完成,CPU将结果传送出来。
CPU只能访问其寄存器(Register)和内存(Memory), 无法直接访问硬盘(Disk)。 存储在硬盘上的数据必须首先传输到内存中才能被CPU访问。从访问速度来看,对寄存器的访问非常快,通常为1纳秒; 对内存的访问相对较慢,通常为100纳秒(使用缓存加速的情况下);而对硬盘驱动器的访问速度最慢,通常为10毫秒。
当一个程序加载到内存中时,它由四个内存区域组成:
- 堆栈(Stack):存储由该程序的每个函数创建的临时变量
- 堆(Heap):该区域特别适用于动态内存分配
- 数据(Data):存储该程序的全局变量和静态变量
- 代码(Code):存储该程序的指令
2.有哪些内存管理技术
-
Base and limit registers(基址寄存器和界限寄存器)
- 通过基址寄存器和限制寄存器限制进程的内存访问位置和范围。
-
Virtual memory(虚拟内存)
- 所有程序都使用虚拟内存地址
- 虚拟地址会被转换为物理地址
- 物理地址表示数据的实际物理位置
- 物理位置可以是内存或磁盘
-
Swapping(交换)
- 把一部分硬盘空间当内存使用
- Linux中,在内存不够的情况下,操作系统先把内存中暂时不用的数据,存到硬盘的交换空间,腾出内存来让别的程序运行,和Windows的虚拟内存(pagefile.sys)的作用是一样的。这块地方叫做 交换空间(Swap Space)
- Segmentation(分段)
-
Paging(分页)
- 将物理内存划分为多个大小相等的块,称为帧(Frame)。并将进程的逻辑内存空间也划分为大小相等的块,称为页面(Page)
- 任何进程中的任何页面都可以放入任何可用的帧中。
3.Android的进程内存分配
Android 平台在运行时不会浪费可用的内存。它会一直尝试利用所有可用内存。
内存类型
- RAM 是最快的内存类型,但其大小通常有限。高端设备通常具有最大的 RAM 容量。
- zRAM 是用于交换空间的 RAM 分区。所有数据在放入 zRAM 时都会进行压缩,然后在从 zRAM 向外复制时进行解压缩。这部分 RAM 会随着页面进出 zRAM 而增大或缩小。设备制造商可以设置 zRAM 大小上限。
- 存储器中包含所有持久性数据(例如文件系统等),以及为所有应用、库和平台添加的对象代码。存储器比另外两种内存的容量大得多。在 Android 上,存储器不像在其他 Linux 实现上那样用于交换空间,因为频繁写入会导致这种内存出现损坏,并缩短存储媒介的使用寿命。
内存页面
RAM 分为多个“页面”。通常,每个页面为 4KB 的内存。 系统会将页面视为“可用”或“已使用”。可用页面是未使用的 RAM。已使用的页面是系统目前正在使用的 RAM,并分为以下类别:
- 缓存页:有存储器中的文件(例如代码或内存映射文件)支持的内存。缓存内存有两种类型:
- 私有页:由一个进程拥有且未共享
- 干净页:存储器中未经修改的文件副本,可由
kswapd
删除以增加可用内存 - 脏页:存储器中经过修改的文件副本;可由
kswapd
移动到 zRAM 或在 zRAM 中进行压缩以增加可用内存
- 干净页:存储器中未经修改的文件副本,可由
- 共享页:由多个进程使用
- 干净页:存储器中未经修改的文件副本,可由
kswapd
删除以增加可用内存 - 脏页:存储器中经过修改的文件副本;允许通过
kswapd
或者通过明确使用msync()
或munmap()
将更改写回存储器中的文件,以增加可用空间
- 干净页:存储器中未经修改的文件副本,可由
- 私有页:由一个进程拥有且未共享
- 匿名页:没有存储器中的文件支持的内存(例如,由设置了
MAP_ANONYMOUS
标记的mmap()
进行分配)- 脏页:可由
kswapd
移动到 zRAM/在 zRAM 中进行压缩以增加可用内存
- 脏页:可由
内存不足管理
内核交换守护进程(kswapd)
内核交换守护进程 (kswapd
) 是 Linux 内核的一部分,用于将已使用内存转换为可用内存。当设备上的可用内存不足时,该守护进程将变为活动状态。Linux 内核设有可用内存上下限阈值。当可用内存降至下限阈值以下时,kswapd
开始回收内存。当可用内存达到上限阈值时,kswapd
停止回收内存。
-
kswapd
可以删除干净页来回收它们,因为这些页受到存储器的支持且未经修改。如果某个进程尝试处理已删除的干净页,则系统会将该页面从存储器复制到 RAM。此操作称为“请求分页”。 -
kswapd
可以将缓存的私有脏页和匿名脏页移动到 zRAM 进行压缩。这样可以释放 RAM 中的可用内存(可用页面)。如果某个进程尝试处理 zRAM 中的脏页,该页将被解压缩并移回到 RAM。如果与压缩页面关联的进程被终止,则该页面将从 zRAM 中删除。如果可用内存量低于特定阈值,系统会开始终止进程。
低内存终止守护进程
当 kswapd
无法为系统释放足够的内存时,系统会使用 onTrimMemory()
通知应用内存不足(通过继承 ComponentCallbacks2
可以监听到此回调,这时对不可见,不关键的资源进行释放,如Glide在收到此回调时会对缓存的图片进行释放),应该减少其分配量。如果内存依旧不足,内核会终止LRU中的进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。
LMK 使用一个名为 oom_adj_score
的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。
终止顺序:
- 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高
oom_adj_score
的应用开始终止后台应用。 - 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
- 主屏幕应用(桌面):这是启动器应用。终止该应用会使壁纸消失。
- 服务:服务由应用启动,可能包括同步或上传到云端。
- 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
- 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
- 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
- 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
- 原生:系统使用的极低级别的进程(例如,
kswapd
)。
4.Android内存分配机制
Android 运行时 (ART) 和 Dalvik 虚拟机使用分页和内存映射来管理内存
垃圾回收
ART 或 Dalvik 虚拟机之类的受管内存环境会跟踪每次内存分配。一旦确定程序不再使用某块内存,它就会将该内存重新释放到堆中,无需程序员进行任何干预。这种回收受管内存环境中的未使用内存的机制称为“垃圾回收”。垃圾回收有两个目标:在程序中查找将来无法访问的数据对象,并回收这些对象使用的资源。
Android 的内存堆是分代的,这意味着它会根据分配对象的预期寿命和大小跟踪不同的分配存储分区。例如,最近分配的对象属于“新生代”。当某个对象保持活动状态达足够长的时间时,可将其提升为较老代,然后是永久代。
堆的每一代对相应对象可占用的内存量都有其自身的专用上限。每当一代开始填满时,系统便会执行垃圾回收事件以释放内存。垃圾回收的持续时间取决于它回收的是哪一代对象以及每一代有多少个活动对象。
共享内存
- 每个应用程序进程都是从名为Zygote的现有进程分叉(fork)出来的。 Zygote进程在系统引导并加载framework代码和资源(例如Activity Themes)时启动。 要启动新的应用程序进程,系统会fork Zygote进程,然后在新进程中加载并运行应用程序的代码。 这种方法允许在所有应用程序进程中共享大多数的为framework代码和资源分配的RAM页面。
- 大多数静态数据会内存映射到一个进程中。这种方法使得数据不仅可以在进程之间共享,还可以在需要时换出。静态数据示例包括:Dalvik 代码(通过将其放入预先链接的
.odex
文件中进行直接内存映射)、应用资源(通过将资源表格设计为可内存映射的结构以及通过对齐 APK 的 zip 条目)和传统项目元素(如.so
文件中的原生代码)。 - 在很多地方,Android 使用明确分配的共享内存区域(通过 ashmem 或 gralloc)在进程间共享同一动态 RAM。例如,窗口 surface 使用在应用和屏幕合成器之间共享的内存,而光标缓冲区则使用在内容提供器和客户端之间共享的内存。
分配和回收内存
- Dalvik 堆局限于每个应用进程的单个虚拟内存范围。这定义了逻辑堆大小,该大小可以根据需要增长,但不能超过系统为每个应用定义的上限。手机出厂时,会设置堆内存相关的几个系统参数(每个厂商和机型的设定可能不同)通过
adb shell getprop
(具体参考)
[dalvik.vm.heapgrowthlimit]: [192m]// 单个进程的最大可用堆(不包含Native堆)
[dalvik.vm.heapmaxfree]: [8m]// 堆最大空闲内存
[dalvik.vm.heapminfree]: [512k]// 堆最小空闲内存
[dalvik.vm.heapsize]: [512m]// 理论上可用的最大内存
[dalvik.vm.heapstartsize]: [8m]// 表示进程启动后,堆得初始内存大小,它影响初始启动的流畅度。
[dalvik.vm.heaptargetutilization]: [0.75]// 内存利用率
- 堆的逻辑大小与堆使用的物理内存量不同。在检查应用堆时,Android 会计算按比例分摊的内存大小 (PSS) 值,该值同时考虑与其他进程共享的脏页和干净页,但其数量与共享该 RAM 的应用数量成正比。此 (PSS) 总量是系统认为的物理内存占用量。
限制应用内存
- 为了维持多任务环境的正常运行,Android 会为每个应用的堆大小设置硬性上限。可以通过调用
getMemoryClass()
获取应用堆的可用字节兆 - Android 会将非前台应用保留在缓存中,如果用户返回该应用,系统就会重复使用该进程,从而加快应用切换速度。当系统资源(如内存)不足时,系统还会考虑终止占用最多内存的进程以释放 RAM。
5.管理应用内存
监控可用内存和内存使用量
通过继承
ComponentCallbacks2
接口或Activity
中实现onLowMemory(),onTrimMemory()
等方法 监听系统内存相关事件,释放对象以响应指示系统需要回收内存的应用生命周期事件或系统事件。-
通过
ActivityManager.MemoryInfo(),Debug.MemoryInfo()
等获取App内存信息fun getAppMemoryInfo(context: Context){ val am = context.getActivityManager() val outInfo = ActivityManager.MemoryInfo() am.getMemoryInfo(outInfo) // 空闲内存 val availMem = outInfo.availMem val processMemoryInfo = am.getProcessMemoryInfo(Process.myPid()) // 独占内存大小 (USS):应用使用的非共享页面数量(不包括共享页面) val uss = processMemoryInfo?.totalPrivateDirty // 按比例分摊的内存大小 (PSS):应用使用的非共享页面的数量加上共享页面的均匀分摊数量(例如,如果三个进程共享 3MB,则每个进程的 PSS 为 1MB) val pss = processMemoryInfo?.totalPss // 常驻内存大小 (RSS):应用使用的共享和非共享页面的数量 val rss = processMemoryInfo?.totalSharedDirty }
优化代码
- 谨慎使用服务:在您启动某项服务后,系统更倾向于让此服务的进程始终保持运行状态。这种行为会导致服务进程代价十分高昂,因为一旦服务使用了某部分 RAM,那么这部分 RAM 就不再可供其他进程使用。这会减少系统可以在 LRU 缓存中保留的缓存进程数量,从而降低应用切换效率。当内存紧张,并且系统无法维护足够的进程以托管当前运行的所有服务时,这甚至可能导致系统出现抖动。
-
使用经过优化的数据容器:使用
SparseArray
等经过优化的容器 - 谨慎对待代码抽象:通常它们需要更多的代码才能执行,需要更多的时间和更多的 RAM 才能将代码映射到内存中。
- 避免内存抖动
移除会占用大量内存的资源和库
- 缩减总体 APK 大小
- 谨慎使用外部库(第三方库应该了解其实现方式)
参考
Android 内存管理机制
内存管理概览
管理应用内存
认真分析mmap:是什么 为什么 怎么用
谈谈Android的内存管理机制
ART堆大小设置及动态调整过程分析(Android 8.1
进程间的内存分配
查看基于Android 系统单个进程内存、CPU使用情况的几种方法