最近公司的线上代码出现了持续性的内存增长,已经恶化到需要定时重启来解决。使用valgrind检测不出内存泄露,自己写了一个类似bound checker内存泄露的检测工具(更小更灵活),倒是track到一些泄露,但仍然不符合泄露的量级。最后估计到有可能是glibc的内存分配机制导致的内存碎片问题,heap的孔洞很多,但free的时候并不能归还到操作系统,于是对市面上的一些malloc进行调研,分析其内存管理机制,希望在替换malloc后问题得到改善。
系统结构
系统下经典内存布局如上,程序起始的1GB地址为内核空间,接下来是向下增长的栈空间和向上增长的mmap地址。而堆地址是从底部开始,去除ELF、数据段、代码段、常量段之后的地址并向上增长。纵观各种内存布局,对于大内存各种malloc基本上都是直接mmap的。而对于小数据,则通过向操作系统申请扩大堆顶,这时候操作系统会把需要的内存分页映射过来,然后再由这些malloc管理这些堆内存块,减少系统调用。而在free内存的时候,不同的malloc有不同的策略,不一定会把内存真正地还给系统,所以很多时候,如果访问了free掉的内存,并不会立即Run Time Error,只有访问的地址没有对应的内存分页,才会崩掉。
Ptmalloc (glibc 的malloc实现)
小内存分配与释放
在ptmalloc中采用chunk进行小内存管理,并且把相似(相同)大小的chunk组织在一个链表中进行维护,这个链表叫做bin。 前64个bin中组织的chunk大小按8个字节递增,这一块叫做 small_bin。 之后的就是large_bin, large_bin中的chunk是先按size,size相同则按照最近使用时间排列,这样要搜索一个可用的内存时,就在bins里按大小搜索,返回一个最小可用的chunk。
chunk结构如下图所示。可以理解成链表中一个node。存储了上一个相邻的chunk的大小以及flag,还有下一个chunk的“指针”。 flag A 表示是不是在主分配去,M表示是否是mmap得到的, P表示上一个chunk是否在使用中。
在free的时候,ptmalloc会检查附近的chunk,如果标志位为P,即空闲中,会尝试把连续空闲的chunk合并成一个大的chunk,放到unstored bin里。但是当很小的chunk释放的时候,ptmalloc会把它并入fast bin中。同样,某些时候,fast bin里的连续内存块会被合并并加入到一个unsorted bin里,然后再才进入普通bin里。所以malloc小内存的时候,是先查找fast bin,再查找unsorted bin,最后查找普通的bin,如果unsorted bin里的chunk不合适,则会把它扔到bin里。
大内存分配与释放
Ptmalloc的分配的内存顶部还有一个top chunk,如果前面的bin里的空闲chunk都不足以满足需要,就是尝试从top chunk里分配内存。如果top chunk里也不够,就要从操作系统里拿了,这样会造成heap增大。
还有就是特别大的内存,会直接从系统mmap出来,不受chunk管理,这样的内存在回收的时候也会munmap还给操作系统。
总结
- 分配与释放
- 小内存:(相当于实现了自身的cache,速度快)
- 分配:[获取分配区(arena)并加锁] -> fast bin -> unsorted bin -> small bin -> large bin -> top chunk -> 扩展堆 (brk分配)
- 释放:brk分配的内chunk list,只能从top开始线性向下释放。释放掉中间的chunk,无法归还给OS(内存孔洞),而是并链入到了bins/fast bins的容器中。
- 大内存: (与os直接打交道,速度慢)
- 分配:mmap从操作系统获得
- 释放:munmap归还给操作系统
- 小内存:(相当于实现了自身的cache,速度快)