ptmalloc总结
内存管理的一般方法
- 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的大小随着切割动态变化。
2. 主分配区
主分配区是唯一能够映射进程 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直接分配
- 多线程不友好,频繁加锁。