从一个问题谈起:从内核中的伙伴系统,页高速缓存系统,slab内存管理系统,常规内存高速缓存系统,到用户线性区管理,用户动态内存分配malloc/free,最终因时制宜选择自定义内存区管理策略,到底有哪些驱动力?
接下来我们来梳理一下
1.伙伴系统
伙伴系统是内核为解决外碎化问题引入的内存管理机制。在32位体系结构中,虚拟内存空间的第四个GB用来线性的映射物理内存开始的DMA和低端内存管理区。而内存管理的基本单位是页,一个页的大小为4kB。所谓的外碎化指的是多次申请多个页的内存并释放后,会导致内存中存在不间隔的无法集中利用的页,其基本单位仍然是页,只是没有办法找到连续的可用来分配的多个页框。为了应对这样的事情,伙伴系统应运而生。伙伴系统首先将内存分为11个不同的2指数个大小的内存对象集合,每个集合用双向链表表示。分配内存时从小到大选择第一个能够满足大小的内存对象(2的order指数个),在这个过程中,如果没有办法找到适配的块,则对于大块的内存需要分割,分割时候将剩下的2k-2order大小的内存区分别放入到k~order大小的内存对象集合中,如果发现伙伴(相邻的)中有空闲的内存块,则进行合并。释放内存块的时候同理。
- 伙伴系统有效的解决了大块内存的分配问,使得内存管理高效
2.页高速缓存机制
在cpu高速缓存往往是提高整个内存访问的关键所在,所以对于单页管理我们采取单独的机制来运行。其中包括热高速换粗和冷高速缓存,热高速缓存是用来保存经常存取的数据的。将内存中分散的单页缓存到高速缓存中进行管理能够有效的提高内存分配合释放的效率。
slab内存管理系统,slab系统也是一种有效的高速缓存机制,只是这是一种更加细粒度的内碎化避免机制。它将内存管理的粒度降低到实际分配数据类型的规模。同样的,在高速缓存中维护着多个大小不同的slab对象集合,集合使用空闲,使用,部分空闲等三个链表进行管理,使用三个链表能够有效的分配和释放内存。同时每一个cpu高速缓存都维护着自己的常规高速缓存(相当于维护了一个常用内存对象的集合,用于更加高效的内存管理)
总体而言这些都是在内存利用率和和性能之间寻找折中方案的内存管理策略,管理的粒度越小,内存利用率越高,但是相应的内碎化就越严重,进一步又制约了大块内存分配的效率,而管理的粒度越大,带来的内存管理的开销变小了,但是内存的利用率会衰减的很严重。
用户动态内存的分配
malloc/free是用户动态内存分配的libc接口,传统的堆内存分配机制使用brk()(底层类似于mmap的机制)系统调用来从操作系统获得内存。往往初始化会分配128k内存区,然后统一管理者128k的内存。到这一步,内存管理的开销就仅仅取决于算法层面的设计了。malloc中将内存分成不同的大小的块(但是得保证对8字节对齐,字),每一次malloc都会按照给定的大小寻找合适的块。这里我们有两种选取策略,第一种是选择第一个满足条件的内存块(first fit),第二种是选择最适配的块(best fit);这两种大小各有优劣,标准采用的是第一种 ,但是在其之上使用的相应的优化算法,如块的分裂;
Ptmalloc2是glibc malloc的多线程拓展:
因为系统调用的代价很高,用户申请内存不能每次都从内核分配,尤其对小内存。并且因为mmap的区域容易被munmap释放,所以一般大内存采用mmap(),小内存使用brk().
事实上,brk申请内存的方式和mmap申请匿名空间的内存映射的机制差不多,只不过前者维护一个边界指针,只会上下滑动扩展或者收缩内存,无法随机分配或者分配内存。
ptmalloc2拥有一个主分配区和多个非主分配区。非主分配区使用mmap向操作系统批量申请HEAP_MAX_SIZE大小的虚拟内存。
对于多线程的支持:当其中某一个线程调用malloc的时候,会先查看线程的私有变量中是否已经存在一个分配区,如果存在则尝试加锁,如果加锁失败则遍历areana链表(分配区组成的环形链表)试图获取一个没有加锁的arena,如果依然获取不到就创建一个新的非主分配区。
free释放的时候也要获取锁。分配小块内存容易产生碎片,ptmalloc在整理合并的时候要对arena做加锁操作。在线程多的时候,锁的开销就会增大。
用户请求分配的内存在ptmalloc中使用chunk表示,每一个chunk至少需要8个字节额外的开销。用户free掉的内存不会马上归还给操作系统,ptmalloc会同一管理heap哈mmap区域的空闲chunks,避免了频繁的系统调用。
ptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin,维护了128个内存对象的bins,用数组存储:
数组中的第一个为 unsorted bin, 数组中从 2 开始编号的前 64 个 bin 称为 small bins, 同一个small bin中的chunk具有相同的大小。small bins后面的bin被称作large bins。
当free一个chunk的时候,ptmalloc还会检查它前后的chunk是否是空闲的,如果是的话,ptmalloc会首先把他们合并成为一个大的chunk,然后将合并后的chunk放到unsortedbin中。另外ptmalloc为了提高分配的速度,会把一些小的(不大于64B)chunk先放到一个fast bin中
在fast bins和bins都不能满足需求之后,ptmalloc会设法在一个叫做top chunk的空间分配内存。对于非主分配区会预先通过mmap分配一块内存作为top chunk,当bins和fast bins都不能满足分配需要的时候,ptmalloc会设法在top chunk迁移到的chunk上,并用单链表链接起来。如果free的chunk恰好与top chunk相邻,那么这两个chunk就会合并成新的top chunk,如果top chunk大小恰好与top chunk迁移到新的chunk上,并用单链表链接起来。如果top chunk的大小大于某个阈值才还给操作系统。主分配区类似,不过通过sbrk()分配和调整top chunk的大小,只有heap顶部连续内存空间超过阈值的才会回收内存。
ptmalloc存在的问题:
1、后分配的内存先释放,因为ptmalloc收缩内存是从top chunk开始,如果与top chunk相邻的chunk不能释放,top chunk以下的chunk都无法释放。
2、多线程开销太大,需要避免多线程频繁的分配释放
3、内存从thread的arena中分配,内存不能从一个arena移动到另外一个arena。就是说如果多线程使用内存不均衡,容易导致内存的浪费。
4、不定期分配长生存周期的内存容易产生大量的内存碎片,不利于回收。64位操作系统最好分配32位以上的内存,这是使用mmap的阈值。
内存池拓展一:tcmalloc——golang的内存管理机制
简介:提出,tcmalloc是google开源的一个内存管理库,作为glibc malloc的替代品。目前已经在chrome和safari等知名软件中使用。
小对象分配:tcmalloc为每一个线程分配一个线程本地Thread Cache,小内存从ThreadCache中分配,此外还有一个中央堆,ThreadCache不够用的时候,会从CentralCache中获取空间放到threadcache中。
小对象(<=32KB)从threadcache中分配,大对象从centralcache中分配。大对象分配的空间都是4k对齐的多个pages也能分割成多个小对象划分到threadcache中。小对象有将近170个不同的大小分类,每一个class维护一个该大小内存块的的freelist的单链表,分配的时候先找到bestfit的class,然后无锁的获取该链表首个首元素返回。如果链表中悟无空间了,则到centralCache中划分几个页面并分割成该class的大小,放入链表中。
CentralCache分配管理
大对象(32k)先4k对齐后,从centralCache中分配。CentralCache维护的PageHeap,数组中的第256个元素是所有大于255个页面都挂在该链表上。
当best fit的页面链表没有空闲时,则一直往更大的页面空间找,如果256个链表都遍历后依然没有成功分配。则使用sbrk,mmap,、/dev/mem从系统中分配
tcmalloc PageHeap管理的连续的页面叫做span,如果span是pageHeap中的一个链表元素,如果span已经分配,他可能是返回给应用程序的大对象,或者已经被分割成了多个小对象,该小对象的size-class会被记录在span中
在32位系统中,使用一个中央数组映射了页面和span的对应关系,数组索引是页面好,数组元素是页面所在的span。在64位系统中,使用一个三级 radix tree记录了该映射关系。
回收
当一个object free时候,会根据地址对齐计算所在的页面号,然后通过central array找到对应的span
如果是小对象,span会告诉我们他的size class,然后把该对象插入当前进程的threadCache中,如果此时ThreadCache超过一个预算值,则会使用垃圾回收机制把未收用object从threadcCached移动到CentralCache的central free lists中
如果是大对象,span会告诉我们对象锁在页面号范围。假设这个范围是[p,q], 先查找页面p-1和q+1所在的span,如果这些临近的span也是free的,则合并到[p,q]所在的span, 然后把这个span回收到PageHeap中。
CentralCache的central free lists类似ThreadCache的FreeList,不过它增加了一级结构,先根据size-class关联到spans的集合, 然后是对应span的object链表。如果span的链表中所有object已经free, 则span回收到PageHeap中。
改进:
- threadCache会阶段性的回收内存到CentralCache里。解决了ptmalloc2中arena间不能迁移的问题。
- Tcmalloc占用更少的额外空间,例如N个8字节对象可能要使用大约8N*1.01字节的空间。即,多用百分之一的空间。Ptmalloc2使用8字节描述一个chunk。
- 更快。小对象几乎无锁,大于32KB的对象从CentralCache中分配使用自旋锁。并且>32KB对象都是页面对齐分配,多线程的时候应该尽量避免频繁分配,否则造成自旋锁的竞争和页面对齐造成的浪费。
jemalloc
简介:jemalloc是facebook推出的,最早的时候的freebsd的libc malloc实现。目前firefox、Facebook服务器各种组件中大量使用。
jemalloc原理:
- 与tcmalloc类似,每一个线程同样在<32KB的时候无锁使用线程本地cache。
- jemalloc在64bits系统上使用下面的size-class分类:
small:
large:
huge:
references:
https://blog.csdn.net/junlon2006/article/details/77854898
https://blog.csdn.net/junlon2006/article/details/77854898
有疑问加站长微信联系(非本文作者)