咱们知道,手机的内存是有限的,假如运用内存占用过大,轻则引起卡顿,重则导致运用溃散或被体系强制杀掉,更严峻的状况下会影响运用的留存率。因此,内存优化是功能优化中非常重要的一部分。但是,许多开发者对内存的知道还停留在运用开发这一层,平时仅仅参考网上的计划,对内存进行比较浅显的优化。想要深入进行内存优化,咱们需求从操作体系的层面了解内存是怎样办理的,又是怎么被运用的。
可能会有人疑问:“为什么做个内存优化需求从操作体系层了解内存呢?”咱们确实能够在网上搜到许多内存优化的文章,但它们都是从上层运用出发进行优化的,而不同的运用由于环境不一样、事务不一样,许多优化方法都不能通用。因此,只要当咱们从底层把握了内存的原理,从下而上地拟定优化计划,才能适用于任何事务,甚至当咱们转型到 iOS、前端或许后端都能通用。
接下来,咱们就从操作体系底层出发,重新知道内存。
咱们先将目光放到操作体系的前期,在这个环境下,程序都是直接操作物理内存的。比方一个程序履行如下指令:
MOV REGISTER1,0
计算机会将位置为 0 的物理内存中的内容移到 REGISTER1 的寄存器中。在这种状况下,假如第二个程序在 0 的位置写入一个新的值,就会擦掉第一个程序寄存在相同位置上的一切内容,导致第一个程序溃散。
正由于运用程序能够直接操作物理内存,所以咱们完全能够修改其他程序在内存中的数据,导致程序溃散或许产生安全问题。因此,对其时的操作体系来说,一同运行多个程序很困难。
为了处理这个问题,咱们自然而然会想到:不答应运用程序直接操作物理内存。于是虚拟内存的技能诞生了。
为了更好地了解什么是虚拟内存,咱们先看看前期直接操作物理内存体系下的内存模型长什么样。从下面内存模型的简化图中咱们能够看到,物理内存中存在两块数据,一个是操作体系的数据,一个是运用程序的数据。除此之外,其实还会有设备驱动程序的数据,它们不是咱们了解的要点就先不列上去了。
什么是虚拟内存?
虚拟内存技能相当于给每个程序一个独占且接连的内存,比方 32 位体系下是 4G(2^32),只不过这个内存是虚拟的。一同,虚拟内存需求能够映射到真实的物理内存。简化的内存模型如下
从上图的简化内存模型,咱们能够看到,每个程序都能够独享一块虚拟内存。在 Linux 体系上,一个进程代表着一个程序,这儿咱们能够理解成每个进程都独享一块虚拟内存,这块虚拟内存在 32 位体系下是 4G(2^32),64 位体系下是 2^48,即 256TB(这儿不是 2^64,是由于 256TB 现已足够大了,假如用 2^64,会有很多的寻址空间浪费)。
其次,虚拟内存都由运用程序和操作体系这两部分组成。其中,运用程序这部分虚拟内存是运用独占的,操作体系这部分虚拟内存则由一切进程同享,而一切进程的操作体系这部分虚拟内存都指向了同一段物理内存。虚拟内存到物理内存的映射由操作体系来完结,操作体系在做映射操作时会寻觅可用的物理内存,不可能呈现掩盖其他数据的状况,这让一同运行多个程序成为了可能。
上图的虚拟内存是一个简化的模型。实践上,虚拟内存和物理内存都是按照页来办理和映射的,一页的巨细为 4KB,咱们来看一个 32 位 Android 体系、物理内存为 2G 的设备的内存模型。
能够看到,物理内存和虚拟内存是通过 4K 巨细的页一一对应的。这个时分,假如咱们再用文章最开头的那个指令:
MOV REGISTER1,0
在虚拟内存技能的加持下,此刻计算机就不会直接将物理地址为 0 的内存移到 REGISTER1 寄存器了,而是先寻觅虚拟地址 0 对应的物理地址 4096,然后将物理地址为 4096 的内容移到 REGISTER1 寄存器中
MOV REGISTER1,4096
虚拟地址转化成物理地址是由计算机的内存办理单元(MMU)完结的,它归于硬件部分而不是体系软件部分,所以转化速度很快。
虚拟内存的内存模型
知道了虚拟内存由操作体系和运用程序两部分组成,而且虚拟内存都由页来保护和办理之后,咱们再深入了解一下 Linux 体系中虚拟内存的内存模型,它需求和 Linux 体系的可履行文件,也便是 ELF 文件一同配合来看。
为了让你理解起来更简单,这儿的 ELF 文件格局也被我简化了,后面用到的时分再进行深入介绍。在 Linux 体系中,寄存操作体系的虚拟内存区域被称为内核空间,剩余的寄存运用的虚拟内存区域称为用户空间。 内核空间占用了 1G,坐落虚拟地址的高地址区域,而 ELF 文件的一些数据,是寄存在低地址的区域(即从地址 0 开端)。下面我详细解释一下内存模型中的几个区域
-
栈:由编译器自动分配开释 ,寄存函数的参数值,局部变量的值等。
-
堆:动态内存分配,能够由开发者自己分配和开释(malloc 和 free 函数完结),Android 开发时不需求咱们手动分配和开释,由于虚拟机程序现已帮咱们做了。堆的开端地址由变量 start_brk 描绘,堆的当前地址由变量 brk 描绘。
-
BSS:寄存全局未初始化,静态未初始化数据。
-
数据段:寄存全局初始化,静态初始化数据。
-
程序代码区:寄存的是 ELF 文件代码段。
能够看到,栈内存的分配是从上到下的,而堆内存的分配是从下到上的,这种方式能够最大程度运用虚拟内存的空间。
虚拟内存分配
前面咱们现已知道,运用是无法直接操作物理内存的,所以咱们在开发 App 时分配的内存实践都是虚拟内存。那么咱们怎样请求虚拟内存呢?
开发 Android 运用时,并不需求咱们自己去分配内存,直接 new 一个目标,声明一个变量或许常量即可,也不需求咱们自己去做开释,但一切的数据都需求内存,这些都是虚拟机帮咱们做。虚拟机分配请求内存主要运用的是 malloc() 函数,它是 C 言语库的一个标准函数。
void *malloc(size_t size)
malloc 函数是一个 C 言语库的函数,所以它分配内存最终还是得调用 Linux 体系提供的函数,让 Linux 内核去帮咱们请求一块内存。内核会调用 mmap() 函数,在堆中分配咱们想要的内存空间巨细。 mmap() 函数是 Linux 体系一个很重要的函数,咱们需求深入知道它。
void *mmap(void *addr,size_t length,int prot,int flags,int fd, off_t offset);
- 参数 addr 指向欲映射的内存起始地址,通常设为 NULL,代表让体系自动选定地址,映射成功后回来该地址;
- 参数 length 表明将文件中多大的部分映射到内存;
- 参数 prot 指定映射区域的读写权限;
- 参数 flags 指定映射时的特性,如是否答应其他进程映射这段内存;
- 参数 fd 指定映射内存的文件描绘符;
- 参数 offset 指定映射位置的偏移量,一般为 0。
mmap 函数有 2 种用法:
- 映射磁盘文件到用户空间中;
-
匿名映射,不映射磁盘文件,而是向映射区请求一块内存,此刻的 fd 入参传 -1。
第 1 种用法能够让咱们读文件的功率更高(比方 Android 读取 dex 文件便是通过 mmap 来进步读取速度),也能够用来完结数据跨进程传输(比方 Android 同享内存机制、Binder 通讯都是通过 mmap 来完结的)。malloc() 函数运用了 mmap 函数的第 2 种用法,即在 Heap 区域中请求一块内存。
需求注意的,这儿请求的内存都是虚拟内存,而且这个时分并不会分配真实的物理内存,只要当咱们真实要往这块虚拟内存区域写入数据时,操作体系检查到对应的虚拟内存没有映射到物理内存,便会产生缺页中止,然后分配一块相同巨细的物理内存,并树立映射联系。这是一种懒加载技能,也是内存优化的计划之一。
malloc() 函数在请求内存小于 128k 时会运用 sbrk() 函数,sbrk() 会将堆顶指针(即前面提到的 brk)向高地址移动,获得新的虚存空间,这些策略都是根据功能考虑的。比方 Android 虚拟机在分配大目标时,也会专门放在 LargeObjectSpcace 中,这些就不打开讲了。至于 Linux 体系是怎么产生缺页中止,怎么分配物理内存,怎么树立映射联系的,都归于 Linux 体系相关知识了,更详细的知识点会在后面的篇章中结合实战项目穿插着解说。
小结
事实上,直接操作物理内存的操作体系并没有消失,咱们现在的嵌入式设备,如冰箱,微波炉等等都能直接操作物理内存。这其实也契合它们的运用场景,直接操作物理内存会让功能开支更小,操作也更便利。但需求一同运行多个软件的体系都有虚拟内存,能够说虚拟内存是现代操作体系最重要的发明之一了。
当咱们重新知道了内存后,咱们再来看内存优化,它其实分为两部分。
- 一是物理内存的优化:也便是这个程序实践消耗的物理内存。
- 二是虚拟内存的优化:在前面咱们也知道了 32 位机只要 3G 的虚拟内存可用,所以一个比较大的Android 程序,很容易就会呈现虚拟内存不足的状况(64 位体系就完全不用担心这个问题)。
在后面的章节中,我会针对这两部分,总结出体系的优化方法论,再调配解说一些优化实践。