内存优化是性能优化的重头戏,因此这部分也花了很多时间来梳理。老规矩,先上大纲:
![ebb4866a7bc38342a54f72660c731240.png](https://img-blog.csdnimg.cn/img_convert/ebb4866a7bc38342a54f72660c731240.png)
1.1 Android内存管理框架:
![ac826c0a9476785f19c91ed9cd9f65ed.png](https://img-blog.csdnimg.cn/img_convert/ac826c0a9476785f19c91ed9cd9f65ed.png)
这里针对上图进行简单描述:
1)物理地址与虚拟地址:
虚拟内存是程序和物理内存之间引入的中间层,目的是解决直接使用物理内存带来的安全性问题、超过物理内存大小需求无法满足等等问题。
而Linux的内存管理就是建立在虚拟内存之上的。虚拟地址与物理地址通过页表建立映射关系,CPU通过MMU(Memory Management Unit :内存管理单元)访问页表来查询虚拟地址对应的物理地址。虚拟地址分为内核空间和用户空间,它们对应的虚拟地址分别为进程共享和进程隔离的。
2)内核空间内存管理:
内核把page作为内存管理的基本单位。对特性不同的page又以zone来做划分,zone又由node来管理。
主要关注的区有3个:
区 | 描述 |
---|---|
ZONE_DMA | 直接内存访问,无需映射 |
ZONE_NORMAL | 一一对应映射页 |
ZONE_HIGHMEM | 动态映射页 |
每个zone中内存的组织形式是基于buddy伙伴算法,把空闲的page以2的n次方为单位进行管理。因此Linux最底层的内存申请都是以2的n次方为单位来申请page的。
Buddy伙伴算法以产生内部碎片为代价来避免外部碎片的产生。Linux针对大内存的物理地址分配,采用Buddy伙伴算法,如果是针对小于一个page的内存,频繁的分配和释放,则不宜用Buddy伙伴算法,取而代之的是Slab。
Slab是为频繁分配/释放的对象建立高速缓存。
3)用户空间内存管理:
用户空间主要分两部分,一个是面向C++的native层,一个是基于虚拟机的java层。
native内存划分:
Data 用于保存全局变量
Bss 用于保存全局未初始化变量
Code 程序代码段
Stack 线程函数执行的内存
Heap malloc分配管理的内存
java基于虚拟机的内存划分:
Program Counter Register 它是一个指针,指向执行引擎正在执行的指令的地址。
VM stack 基于方法中的局部变量,包括基本数据类型以及对象引用等。
Native Method Stack 针对native方法,功能与虚拟机栈一致。
Method Area 虚拟机加载的类信息、常量、静态变量等。
Heap 对象实体。
![20dd2169472967e68c6eb4cdedeeacd5.png](https://img-blog.csdnimg.cn/img_convert/20dd2169472967e68c6eb4cdedeeacd5.png)
1.2 linux内存分配与回收
内存分配:
在调用alloc_page或者alloc_pages等接口进行一次内存分配时,最后都会调用到__alloc_pages_nodemask函数,这个函数是内存分配的心脏,对内存分配流程做了一个整体的组织。该流程牵涉到的分配过程有两个:
快速内存分配:是get_page_from_freelist函数,通过low阀值从zonelist中获取合适的zone进行分配,如果zone没有达到low阀值,则会进行快速内存回收,快速内存回收后再尝试分配。
慢速内存分配:当快速分配失败后,也就是zonelist中所有zone在快速分配中都没有获取到内存,则会使用min阀值进行慢速分配,在慢速分配(slow path)过程中主要做三件事,异步内存压缩、直接内存回收以及轻同步内存压缩,最后视情况进行oom分配。并且在这些操作完成后,都会调用一次快速内存分配尝试获取页框。
内存回收:
内存回收是以zone为单位进行的(也会以memcg为单位,这里不讨论这种情况),而系统判断一个zone需不需要进行内存回收是由水线watermark来判断的。
high 当zone的空闲页框数量高于这个值时,表示zone的空闲页框较多,不需要再继续进行内存回收。
low 快速分配的默认阀值,在分配内存过程中,如果zone的空闲页框数量低于此阀值,系统会对zone执行快速内存回收。
min 在快速分配失败后的慢速分配中会使用此阀值进行分配,如果慢速分配过程中使用此值还是无法进行分配,那就会执行直接内存回收和快速内存回收。
当linux系统内存压力就大时,就会对系统的每个压力大的zone进程内存回收,它针对三样东西进程回收:slab、lru链表中的页、buffer_head。
这里主要看lru链表中的页是怎么回收的。lru链表主要用于管理进程空间中使用的页,类型分为:文件页、匿名页、shmem页。
文件页(file-backed page):有文件背景页面。可以直接和硬盘对应的文件进行交换。
匿名页(anonymous page):无文件背景页面。如进程堆、栈、数据段使用的页等,无法直接跟磁盘交换,但是可以跟swap区进行交换。
mmap页(tmpfs/shmem的page):它具有文件的属性,能够像操作文件一样去操作它。但是它无文件背景,因此也有匿名页属性,内核在内存紧缺时不能简单的将page从它们的page cache中丢弃,而需要swap-out。
![f214d61e7e971f6def4c4b5f9b544fd8.png](https://img-blog.csdnimg.cn/img_convert/f214d61e7e971f6def4c4b5f9b544fd8.png)
三种文件页回收对比,图片出处:[linux内核tmpfs/shmem浅析]
Lru链表回收算法
Lru链表有5个双向链表:
LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE。
老化过程:将不处于lru链表的新页放入到lru链表中->将处于活动lru链表的页移动到非活动lru链表->将非活动lru链表中的页移动到非活动lru链表尾部->回收页然后将页从lru链表中移除。
页回收方式
页回写:文件页保存的数据与磁盘中文件对应的数据不一致,则认定此文件页为脏页,需要先将此文件页回写到磁盘中对应数据所在位置上,然后再将此页作为空闲页框释放到伙伴系统中。
页交换:不经常使用的匿名页,将它们写入到swap分区中,然后作为空闲页框释放到伙伴系统。
页丢弃:文件页中保存的内容与磁盘中文件对应内容一致,说明此文件页是一个干净的文件页,就不需要进行回写,直接将此页作为空闲页框释放到伙伴系统中。
当内存紧张时,优先换出无脏数据的page cache(文件页包含page cache),直接丢弃。其次才是匿名页和有脏数据的文件页的回收。遵循URL老化规则。通过Swappiness来确定更倾向于回收哪种更多一点,swappiness越大,越倾向于回收匿名页,反之越倾向于回收文件页。
内存回收手段
因为在不同的内存分配路径中,会触发不同的内存回收方式,内存回收针对的目标有两种,一种是针对zone的,另一种是针对一个memcg的,而这里我们只讨论针对zone的内存回收,个人把针对zone的内存回收方式分为三种,分别是快速内存回收、直接内存回收、kswapd内存回收。
快速内存回收:处于get_page_from_freelist函数中,在遍历zonelist过程中,对每个zone都在分配前进行判断,如果分配后zone的空闲内存数量 < 阀值 + 保留页框数量,那么此zone就会进行快速内存回收,即使分配前此zone空闲页框数量都没有达到阀值,都会进行此zone的快速内存回收。注意阀值可能是min/low/high的任何一种,因为在快速内存分配,慢速内存分配和oom分配过程中如果回收的页框足够,都会调用到get_page_from_freelist函数,所以快速内存回收不仅仅发生在快速内存分配中,在慢速内存分配过程中也会发生。
直接内存回收:处于慢