最近公司的线上代码出现了持续性的内存增长,已经恶化到需要定时重启来解决。使用valgrind检测不出内存泄露,自己写了一个类似bound checker内存泄露的检测工具(更小更灵活),倒是track到一些泄露,但仍然不符合泄露的量级。最后估计到有可能是glibc的内存分配机制导致的内存碎片问题,heap的孔洞很多,但free的时候并不能归还到操作系统,于是对市面上的一些malloc进行调研,分析其内存管理机制,希望在替换malloc后问题得到改善。
系统下经典内存布局如上,程序起始的1GB地址为内核空间,接下来是向下增长的栈空间和向上增长的mmap地址。而堆地址是从底部开始,去除ELF、数据段、代码段、常量段之后的地址并向上增长。纵观各种内存布局,对于大内存各种malloc基本上都是直接mmap的。而对于小数据,则通过向操作系统申请扩大堆顶,这时候操作系统会把需要的内存分页映射过来,然后再由这些malloc管理这些堆内存块,减少系统调用。而在free内存的时候,不同的malloc有不同的策略,不一定会把内存真正地还给系统,所以很多时候,如果访问了free掉的内存,并不会立即Run Time Error,只有访问的地址没有对应的内存分页,才会崩掉。
在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还给操作系统。
大内存和小内存的界限: mallopt() 函数可以调整threshold,大于此threshold的用mmap分配,小于此值的用brk缓存技术分配。
mallopt(M_MMAP_MAX, 0); // 禁止malloc调用mmap分配内存 mallopt(M_TRIM_THRESHOLD, 0); // 禁止内存缩进,sbrk申请的内存释放后不会归还给操作系统 |
如上的code相当于实现了一个内存池功能,这个会持续到线程结束才把内存归还给操作系统。
M_MMAP_THRESHOLD 的设置是多大算大内存,多大算小内存的设置M_TRIM_THRESHOLD heap顶部攒到多大归还给操作系统,0则是不归还给操作系统
手动shrink heap: 使用malloc_trim()根据man手册的解释,它应该是负责告诉glibc在brk维护的堆队列中,堆顶留下多少的空余空间(free space),其他往上的空余空间全部归还给系统。
手动设置每个线程的arena数量:mallopt(M_ARENA_MAX, 1) ,设置为0则为系统自动设置。
经验教训:
http://goog-perftools.sourceforge.net/doc/tcmalloc.html
Tcmalloc是Google gperftools里的组件之一。全名是 thread cache malloc(线程缓存分配器)。tcmalloc正是通过thread cache这种机制实现了大多数情况下的无锁内存分配。优势如下:
tcmalloc与大多数现代分配器一样,使用的是基于页的内存分配,也就是说,这种内存分配的内部度量单位是页,而不是字节。这种内存分配可以有效地减少内存碎片,同时,也可以增加局部性。此外,也可以使得元数据的跟踪更为简单。tcmalloc定义一页为8K字节,在大多数的linux系统中,一页是4K字节,也就是tcmalloc的一页是linux的两页。
tcmalloc中的内存分配块整体来说分为两类,“小块”和“大块”,“小块”是小于kMaxPages的内存块,“小块”可以进一步分为size classes,而且“小块”的内存分配是通过thread cache或者central per-size class cache而实现。“大块”是大于等于kMaxPages的内存块,“大块”的内存分配是通过central PageHeap实现。
对于小块内存分配,其内部维护了60个不同大小的分配器(实际源码中看到的是86个),和ptmalloc不同的是,它的每个分配器的大小差是不同的,依此按8字节、16字节、32字节等间隔开。在内存分配的时候,会找到最小复合条件的,比如833字节到1024字节的内存分配请求都会分配一个1024大小的内存块。如果这些分配器的剩余内存不够了,会向中央堆申请一些内存,打碎以后填入对应分配器中。同样,如果中央堆也没内存了,就向中央内存分配器申请内存。
在线程缓存内的分配器中分别维护了一个大小固定的自由空间链表,直接由这些链表分配内存的时候是不加锁的。但是中央堆是所有线程共享的,在由其分配内存的时候会加自旋锁(spin lock)。
小内存分配的步骤如下:
在线程内存池每次从中央堆申请内存的时候,分配多少内存也直接影响分配性能。申请地太少会导致频繁访问中央堆,也就会频繁加锁,而申请地太多会导致内存浪费。在tcmalloc里,这个每次申请的内存量是动态调整的,调整方式使用了类似把tcp窗口反过来用的慢启动(slow start)算法调整max_length, 每次申请内存是申请min(max_length,每个分配器对应的num_objects_to_move)个数的内存块。
对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里central page free lists分配。中央堆的内存管理是以分页为单位的,同样按大小维护了256个空闲空间链表central list,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就会直接从系统申请了。
在32位系统中,span分为两级由中央分配器管理。第一级有2^5个节点,第二级是2^15个。32位总共只能有2^20个分页(每个分页4KB = 2^12)。64为中有三级管理。
1. 释放某个object
2. 找到该object所在的span
3. 如果是小对象,归入小对象分配器的空闲链表,如果空闲空间大于指定预置则归还给central free list; 如果是大对象,则会把物理地址连续的前后的span也找出来,如果空闲则合并,并归入central free list。
4 central list也会试图通过释放可用span列表的最后几个span来将不用的空间归还给OS
tcmalloc每个线程默认最大缓存16M空间,所以当线程多的时候其占用的空间还是非常可观的(可能会缓存比glibc更多的内存),在common.h中有几个参数是控制缓存空间的,可以做合理的修改:
降低每个线程的缓存空间,
可以修改common.h中的kMaxThreadCacheSize,比如2M
尽快将free的空间还给central list,可以将kMaxOverages改小一点,比如1
降低所有线程的缓存空间的总大小,可以修改common.h中的kDefaultOverallThreadCacheSize,比如20M
还可以定期让tcmalloc归还空间给OS
#include "google/malloc_extension.h" MallocExtension::instance()->ReleaseFreeMemory(); |
https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919/
首先介绍一下jemalloc中的几个核心概念
Jemalloc 把内存分配分为了三个部分,第一部分类似tcmalloc,是分别以8字节、16字节、64字节等分隔开的small class;第二部分以分页为单位,等差间隔开的large class;然后就是huge class。内存块的管理也通过一种chunk进行,一个chunk的大小是2^k (默认4M)。通过这种分配实现常数时间地分配small和large对象,对数时间地查询huge对象的meta(使用红黑树)。
默认64位系统的划分方式如下:
Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840] (1-57344分为44档)
Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB] (58345-4MB)
Huge: [4 MiB, 8 MiB, 12 MiB, …] (4MB的整数倍)
jemalloc 的内存分配,可分成四类: