起因
下面这段代码执行后,内存有增无减,增加了200M,iOS平台200M不能接受了
// STL 集合类
void test1() {
list<int> mList;
for (int i=0; i<1000000; i++) {
mList.push_back(i);
}
mList.clear();
}
// mList 作用域 {} 内,stack 上的变量由编译器出了 } 自动释放
STL 底层是用 new/delete 分配内存的,new/delete 是基于 malloc/free 分配的,malloc/free 又是基于各个操作系统统一封装,于是我写了下面的测试代码
// 申请 100w 个 100byte 的空间,再释放掉
void test2() {
char **ptr = (char **)malloc(1000000 * sizeof(char *));
for (int i=0; i<1000000; i++) {
*(ptr + i) = (char *)malloc(100 * sizeof(char));
}
for (int i=0; i<1000000; i++) {
free(*(ptr + i));
}
free(ptr);
}
内存依然去到了300M,无减少。 原因:
- Windows 平台调用free,内存会马上降回来。
- Linux 平台调用free,内存不会释放回OS,而是释放回系统的内存缓冲池,进程退出时才释放回OS。(ps:Linux下谷歌有个 tcmalloc 能做到立刻释放到OS,或者malloc_trim(0))
- iOS 平台调用 free 后,也只是释放到系统的内存缓冲池里,进程退出才释放回OS。
这就很麻烦了,iOS平台不能去到太高的内存,不然进程会被kiil的,必须要手动释放。
查了一下,iOS用不了谷歌的tcmalloc,malloc_trim也用不了,只能用 malloc_zone_t 自定义一个缓存区,用完自己销毁,就能够释放内存。如下面这段代码,就能把内存释放会 OS 了。
// 自定义 malloc_zone_t 内申请/释放内存
void test3() {
malloc_zone_t *my_zone = malloc_create_zone(0, 0); // 创建一个 zone
char **ptr = (char **)my_zone->malloc(my_zone, 1000000 * sizeof(char *));
for (int i=0; i<1000000; i++) {
*(ptr + i) = (char *)my_zone->malloc(my_zone, 104 * sizeof(char));
}
for (int i=0; i<1000000; i++) {
my_zone->free(my_zone, *(ptr + i));
}
my_zone->free(my_zone, ptr); // 内存释放回 zone,并不是释放会OS,内存还是占用着
malloc_destroy_zone(my_zone); // 内存释放回 os 层,内存占用减少
}
现在能搞定的是通过创建 zone 内的 malloc/free 能控制内存释放回 OS,又有一个问题来了,STL C++的类不能用 malloc/free,内存依然不能释放会OS,这个问题还在找办法,有人知道麻烦告诉我,thanks。
原理
操作系统管理内存的方式进化:段式 -> 页式 -> 段页式。
段式有内存外部碎片,内存利用率低,于是发明了页式,页式有内部碎片。同时页式管理中,进程不一定要全部在内存当中了,不在的部分是虚拟内存,进程的物理空间也不一定连续,虚拟地址通过页表能算出物理地址,不在内存中产生缺页中断。。。等等
页式管理中,一个页为4KB,只能按页为单位拿内存。假设一个对象只有100 byte,为一个对象分配一个页的内存,有3KB多是浪费的,浪费的部分叫做内部碎片。一个进程有几十万个对象,碎片就非常多了利用率低。
于是发明内存缓冲池,假如申请的内存大于一个页的4KB,那么去OS申请。
假如申请的内存都是很小的,几百字节以内的,那么在缓冲池内申请。释放的时候,只释放回缓冲池,预防下次还要申请。也就是从OS拿部分内存自己进程持有,由自己负责分配。
缓冲池内存管理算法:空闲链表,隐式的空闲链表,位图。
经过上面的代码测试,
默认缓冲池直接malloc/free,速度 70ms,但内存单调增长。
创建和销毁缓冲池,自己的缓冲池内malloc/free,速度 160ms,但内存不增长。
所以缓冲池的出现体现了时间换空间,空间换时间的计算思维。
缓冲池的好处:
1.不用触发系统调用,速度快,2.减少内存碎片,提高利用率。
如果不适当释放内存,容易导致内存单调增长。
iOS 的 OC 对象都是通过 alloc 方法创建的,alloc 方法调用了 allocWithZone,这个 NSZone 底层就是 malloc_zone_t,就是缓冲池。所以给对象分配内存的速度是很快的。
内存管理的一般方法
- C 风格的内存管理程序主要实现 malloc()和 free()函数。
- 内存池是一种半内存管理方法。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己 的内存池。在结束每个阶段时,会一次释放所有内存。
- 引用计数
- 垃圾收集。
垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集 器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的 一组“基本”数据——栈数据、全局变量、寄存器——作为出发点。然后它们尝试去追踪通 过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可 以被销毁并重新使用这些无用的数据。
Glibc存在的某个问题
Glibc 在内存回收方面做得不太好,常见的一个问题,申请很多内存,然后又释放,只是有一小块 没释放,这时候 Glibc 就必须要等待这一小块也释放了,也把整个大块释放,极端情况下, 可能会造成几个 G 的浪费。
内存管理数据结构概述
Main_arena 与 non_main_arena
每个进程只有一个主分配区,但可能存在多个非主分配区,ptmalloc 根据系统对分配区 的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少了。
主分配区可以访问进程的 heap 区域和 mmap 映射区域,也就是说主分配区可以使用 sbrk 和 mmap 向操作系统申请虚拟内存。非主分配区只能使用mmap向操作系统申请虚拟内存。
而且程序线程很多的情况下,锁等待的时间就会 延长,导致 malloc 性能下降。一次加锁操作需要消耗 100ns 左右,正是锁的缘故,导致 ptmalloc 在多线程竞争情况下性能远远落后于 tcmalloc。
chunk的组织
不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使 用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统, 相反,它们也会被表示为一个 chunk,ptmalloc 使用特定的数据结构来管理这些空闲的 chunk。
ptmalloc 在给用户分配的空间的前后加上了一些控制信息,用这样的方法来记录分配的
信息,以便完成分配和释放工作。
空闲 chunk 容器
用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映
射区域中的空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会首先试图在空闲的 chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。ptmalloc 将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共 维护了 128 个 bin,并使用一个数组来存储这些bin
- 数组中的第一个为 unsorted bin
- 数组中从编号2到编号为64的bin 称为 small bins,同 一个 small bin 中的 chunk 具有相同的大小。相邻的small bins中的chunk大小相差为8B。
- Small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 分别包含了一个给定范围 内的 chunk,其中的 chunk 按大小序排列。相同大小的 chunk 同样按照最近使用顺序排列。
Fast Bins
ptmalloc 中在分配过程中 引入了 fast bins,不大于 max_fast(默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins 中
Unsorted Bin
unsorted bin 的队列使用 bins 数组的第一个,如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中。在进 行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则 ptmalloc 会先在 unsorted bin 中查找合适的空闲 chunk,然后才查找 bins。
如果 unsorted bin 不能满足分配要求。malloc 便会将 unsorted bin 中的 chunk 加入 bins 中。然后再从 bins 中继续进行查找和分配过程。
从这个过程可以看出来,unsorted bin可以看做是bins的一个缓冲区,增加它只是为了加快分配的速度。
Top chunk
top chunk对于主分配区和非主分配区是不一样的。
- 非主分配区
对于非主分配区会预先从 mmap 区域分配一块较大的空闲内存模拟 sub-heap, 通过管
理 sub-heap 来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处, 必然存在着一块空闲 chunk, 叫做 top chunk。当bins和fast bin都满足不了用户的需求,ptmalloc会从top chunk分出一块内存给用户,如果top chunk空间不足,会重新分配一个sub-heap,将top chunk迁移到行的sub-heap上。新的sub-heap和旧的sub。在分配过程中,top chunk的大小随着切割动态变化。
- 主分配区
主分配区是唯一能够映射进程 heap 区域的分配区,它可以通过 sbrk()来增大或是
收缩进程 heap 的大小。top chunk在heap的最上面,如果申请内存时,top chunk空间不足,ptmalloc
会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。
mmaped chunk
当分配的内存空间过大的时候,top chunk也不能满足需求。ptmalloc会直接调用mmap给用户分配内存。
mmap分配阈值( mmap threshold,默认值为 128KB,分配阈值可以动态调整。如果开启了 mmap 分配
阈值的动态调整机制,并且当前回收的 chunk 大小大于 mmap 分配阈值,将 mmap
分配阈值设置为该 chunk 的大小,将 mmap 收缩阈值设定为 mmap 分配阈值的 2
倍。
last remainder
last remainder也不存在于bins中,当用户在small bins中找不到合适的chunk,如果last remainder的大小大于small chunk的大小,last remainder会分裂为两个chunk,一个返回给用户,另一个变成新的remainder chunk。
内存分配概述
以32位系统为例
- 小于等于 64 字节:用 pool 算法分配。
- 64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。
- 大于等于 512 字节:用最佳匹配算法分配。
- 大于等于 mmap 分配阈值 (默认值 128KB): 根据设置的 mmap 的分配策略进行分配,
如果没有开启 mmap 分配阈值的动态调整机制,大于等于 128KB 就直接调用 mmap20
分配。 否则,大于等于 mmap 分配阈值时才直接调用 mmap()分配。
ptmalloc 的响应用户内存分配要求的具体步骤
- 获取一个未加锁的分配区,如果所有分配区都加了锁,ptmalloc会开辟一个新的分配区。开辟新分配区时,会调用mmap创建一个sub-heap,并设置好top chunk。
- 将用户的请求大小转换为实际需要分配的 chunk 空间大小
- 判断所需分配 chunk的大小是否满足 chunk_size <= max_fast (max_fast 默认为 64B),如果是的话, 则转下一步, 否则跳到第 5 步。
- 首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。 如果可以找到, 则分配结束。 否则转到下一步。
- 判断所需大小是否处在 small bins 中, 即判断 chunk_size < 512B 是否成立。 如果chunk大小处在small bins中,则转下一步,否则转到第7步。
- 根据所需分配的 chunk 的大小, 找到具体所在的某个 small bin, 从该 bin 的尾部摘取一个恰好满足大小的chunk。若成功,则分配结束,否则,转到下一步。
- 首先尽可能将fast bins中的chunk合并,并且放入unsorted bin中。如果unsorted bin中只有一个chunk,而且在上次分配的时候使用过,并且说需要分配的属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直
接将该 chunk 进行切割,分配结束。否则,将unsorted bin中的chunk放入small bins或者large bins。进入下一步。 - 从large bins分配一块合适的chunk.
- 根据申请空间的大小和mmap分配阈值判断,从top chunk中分配内存还是直接调用mmap分配内存。
总结:
小内存: [获取分配区(arena)并加锁] -> fast bins -> small bins -> 合并fast bins加入unsorted bins -> unsorted bins合并,加入small bins或者large bins -> small bins -> large bins -> top chunk(低于mmap阈值) -> mmap(高于mmap 阈值)
大内存: 直接mmap
内存回收概述
- 根据地址对齐找到sub-heap,从sub-heap头部信息找到属于哪个分配区,获取分配区的锁,保证线程安全。
- 判断所需释放的 chunk 是否为 mmaped chunk,如果是,则调用munmap()释放mmaped chunk,解除内存空间映射,该该空间不再有效。
- 判断 chunk 的大小和所处的位置,若 chunk_size <= max_fast,并且 chunk 并不位于heap 的顶部,也就是说并不与 top chunk 相邻,则转到下一步。否则跳到5。
- 将 chunk 放到 fast bins 中, chunk 放入到 fast bins 中时,并不会修改chunk的使用状态位,也并不尝试合并。然后free函数返回。
- 判断前一个 chunk 是否处在使用中,如果前一个块也是空闲块,则合并。并转下一步。
- 判断当前释放 chunk 的下一个块是否为 top thunk,如果是,则转第8步,否则转下一步。
- 判断下一个 chunk 是否处在使用中, 如果下一个 chunk 也是空闲的, 则合并, 并将合并后的 chunk 放到 unsorted bin 中。
- 将chunk与top chunk合并。
- 判断合并后的chunk的大小是否大于FASTBIN_CONSOLIDATION_THRESHOLD(默认
64KB), 如果是的话,则会触发进行 fast bins 的合并操作, fast bins 中的 chunk 将被遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中。fast bins 将变为空, 操作完成之后转下一步。 - 判断top chunk是否大于mmap收缩值,如果大于就将一部分top chunk归还给操作系统。
总结:
- 大内存:直接munmap
- 小内存(与top chunk相邻):如果在top chunk上面,尽可能合并chunk,然后与top chunk合并。
- 小内存(chunk_size <= max_fast):直接放入fast_bin
- 小内存(chunk_size > max_fast):与周围的chunk合并后放到unsorted bin中。
- (all)如果合并后的chunk触发合并fast bin操作,合并fast bin放到unsorted中。
ptmalloc缺点
- 因为 ptmalloc 收缩内存是从top chunk开始,如果与top chunk相邻的chunk不能释放,top chunk以下都不能释放。对于长期持有的内存,尽量直接越过mmap阈值调用mmap直接分配
- 多线程不友好,频繁加锁。