iOS 内存管理
iOS 内存管理和 malloc 源代码解读
iOS 内存基本管理
在接触 iOS 开发的时候,我们知道引用计数器的概念,也知道 ARC 和 MRC,但其实这仅仅是对堆内存上对象的内存管理。
在内存管理方面,其实 iOS 和其他操作系统总体上来说是大同小异的,大的框架原理基本相似,小的细节有所创新和不同。
和其他操作系统上运行的进程类似,iOS App 进程的地址空间也分为代码区、数据区、栈区和堆区。进程开始时,会把 mach-o 文件中的各部分,按需加载到内存中。
对于一般的 iPhone 来说,实际物理内存都在1GB 左右,对于超大的内存需求,也和一般的操作系统一样,都由系统内核维护一套虚拟内存系统。但这里需要注意的是 iOS 的虚拟系统原则略有不同,最截然不同的地方就是当物理内存内存紧张情况时得处理。
当物理内存紧张时,iOS 会把可以通过重新映射来加载的内存直接清理出内存,对于不可再生的数据,iOS 需要 App 进程配合处理,由各进程发送内存警告要求配合释放内存。对于不能及时释放足够内存的,直接 kill 掉进程,必要时甚至是前台运行的 app。。
所以,iOS 在外存没有交换区,没有内存页换出的过程。
malloc 基本原理
在 iOS APP 进程地址空间各个区域中,最灵活的就要属堆区,它为进程动态分配内存,也是我们经常和内存打交道的地方。
通常,我们会在需要新对象的时候,进行[NSObject alloc]调用,而释放对象时需要 release(ARC 会自动帮你做到这些)。
而这些 alloc、release 方法的调用,最终会走到 libsystem_malloc.dylib 的 malloc() 和 free() 函数这里。libsystem_malloc.dylib 是 iOS 内核之外的一个内存库,我们 app 进程需要的内存,先会请求到这里,但最终libsystem_malloc.dylib也都会向 iOS 的系统内核发起申请,映射实际内存到 app 进程的地址空间。
从苹果公开的 malloc 的源代码上看,原理大概如此:malloc 内存分配基于 malloc zone,并将内存分配按大小分为 nano、tiny、small、large 几种类型。申请时按需进行最适分配。 malloc 在首次调用时,初始化 default zone,在 64 位情况下,会初始化 default zone 为 nano zone。同事初始化一个 scalable zone 作为 helper zone,nano zone 负责 nano 大小的分配,scalable zone 负责 tiny、small 和 large 内存的分配。每次 malloc 时,根据传入的 size 参数,优先交个 nano zone 做分配处理,如果大小不在 nano 范围,则转交给 helper zone 处理。

nano malloc
在支持64位的条件下,malloc 优先考虑 nano malloc,负责对 256B 以下小内存分配,单位是16B。
nano zone 分配内存的地址空间范围是 0x00006nnnnnnnnnnn (OSX 64位情况),将地址空间从大到小一次分为 magazine、band、slot 几个级别
magazine范围对应于CPU,CPU0对应Mag0、CPU1对应Mag1,以此类推;Band范围为2M,连续分配内存当内存不够时以Band为单位向内核请求。Slot则对应每个Band中 128K 大小的范围,每个Band都分为 16个Slot,分别对应于 16B、32B、…. 256B大小,支持他们的内存分配。
分配过程
- 确定当前 CPU 对应的 mag 和通过 size 参数计算出来的 slot,去对应的 metadata 的链表中取已经被释放过得内存区块缓存。判断渠道检查指针地址是否有问题,没有问题就直接返回。
- 初次进行 nano malloc 时,nano zone 并没有缓存,会直接在 nano zone 范围的地址空间上直接分配连续地址内存。
- 如当前 band 中当前 slot 耗尽,则向系统申请新的 band (每个 band 固定大小2M,容纳了16个128K 的槽),连续地址分配内存的基地址、limit 地址以及当前分配到的地址由 meta data 结构维护起来,而这些 meta data 则以 mag、slot 为维度(mag 个数是处理器个数,slot 是16个)的二维数组形式,放在 nanozone_t 的 meta_data字段中。
当 app 通过 free() 释放内存时:malloc 库会检查指针地址,如果没有问题,则以链表形式将这些区块按大小存储起来。这些链表的头部放在 meta_data 数组中对应的 [mag][slot] 元素中。
其实从缓存获取空余内存和释放内存时都会对指向这片内存区域的指针进行检查,如果有类似地址不对齐、未释放、多次释放、所属地址与预期的 mag、slot 不匹配等情况都会以报错结束。

除了分配和释放、系统内存吃紧时,nano zone 需将 cache 的内存区块还给系统,这主要是通过对各个 slot 对应的 meta data 上挂着空闲的链表上内存区块回收来完成。
scalable zone 上内存分配简要分析
对于超出 nano 大小范围或者不支持 nano 分配的,直接会在 scalable zone 上分配。由于 scalable zone 上的内存分配比 nano 分配要复杂。下面只做简单介绍。
scalable zone 上分配的内存包括 tiny、small、large 三大类。其中 tiny 和 small 的分配、释放过程大致相同,large 类型有自己的方式管理。
而 tiny、small 的方式也遵循着 nano 分配中的原则,新内存从系统申请并分配,free 后按照大小以特定的形式缓存起来,供后续分配使用。这里的分配在 region 上进行,region 和 nano malloc 里的 band 概念即为相似,但不同的是地址空间未必连续,而且每个 region 都有自己的位图等描述信息。和 nano 一样每个 CPU 都有一个 magazine,除此之外还分配了一个 index 为 -1 的 magazine 作为后备之用。
以 tiny 的情况为例,
分配时:
- 确定当前线程所在处理器的 magazine index,找到对应的 magazine 结构。
- 优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐后的 slot 大小),如果是则直接返回。
- 如果不是,则查找 free list 中当前请求大小区块的空闲缓存列表,如果有返回,并整理列表。
- 如果没有,则在 free list 找比当前申请区块大的,而且最接近的缓存,如果有返回,并把剩余大小放到 free list 中另外的链表上。(这里需要注意的是,在一般情况下,free list 分为64个槽,0-62 上挂载区块的大小都是按 16B 为单位递增,63为所有更大的内存区块挂载的地方)
- 上面几项都不行,就在最后一个 region 的尾部或者首部(如果支持 ALSR)找空闲区域分配。
- 如果还是不行,说明所有现有 region 都没有可用空间了,那么从一个后备 magazine 中取出一个可用 region,完整地拿过来放到当前 magazine,再走一遍上面的步骤。
- 如果这都不行,那只能向内核申请一块新的 region 区域,挂载到当前的 magazine 下并分配内存。
- 如果还是不行,那就没招了,系统也给不到内存,就返回报错。
free 时
- 检查指针指向的地址是否有问题。
- 如果 last free 指针上没有挂载内存区块,则当道 last free 上就 OK 了。
- 如果有 last free,置换内存,并把 last free 原有内存区块挂载到 free list 上。(在挂载的 free list 前,会根据 region 位图检查前后区块是否能合并成为更大的区块,如果能会合并成一个)
- 合并后所在的 region 如果空闲字节超过一定条件,则把 region 放到后备的 magazine 中(-1)。
- 如果整个 region 都是空的,则直接还给系统内核,一了百了。
而 large 的情况,malloc 以页为单位申请和分配内存,不区分 magazine,scalable zone 同意维护一个 hash table 管理已申请的内存。而且由于内存区域都比较庞大,置换村总量2G 的区块,氛围16个元素,每个最大为128M。large 相关的结构相对简单。