我们知道malloc() 并不是系统调用,也不是运算符,而是 C 库里的函数,用于动态分配内存。
malloc 申请内存的时候,会有两种方式向操作系统申请堆内存:
- 方式一:通过 brk() 系统调用从堆分配内存
- 方式二:通过 mmap() 系统调用在文件映射区域分配内存;
一、brk()系统调用
1、brk()的申请方式
一般如果用户分配的内存小于 128 KB,则通过 brk() 申请内存。而brk()的实现的方式很简单,就是通过 brk() 函数将堆顶指针向高地址移动,获得新的内存空间。
malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用,这样就可以重复使用。
2、brk()系统调用的优缺点
所以使用brk()方式的点很明显:可以减少缺页异常的发生,提高内存访问效率。
但它的缺点也同样明显:由于申请的内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。brk()方式之所以会产生内存碎片,是由于brk通过移动堆顶的位置来分配内存,并且使用完不会立即归还系统,重复使用,如果高地址的内存不释放,低地址的内存是得不到释放的。
正是由于使用brk()会出现内存碎片,所以在我们申请大块内存的时候才会使用mmap()方式,mmap()是以页为单位进行内存分配和管理的,释放后就直接归还系统了,所以不会出现这种小碎片的情况。
3、brk()系统调用的优化
一、Ptmalloc :malloc采用的是内存池的管理方式,Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。
二、Tcmalloc:Ptmalloc在性能上还是存在一些问题的,比如不同分配区(arena)的内存不能交替使用,比如每个内存块分配都要浪费8字节内存等等,所以一般倾向于使用第三方的malloc。
Tcmalloc是Google gperftools里的组件之一。全名是 thread cache malloc(线程缓存分配器)其内存管理分为线程内存和中央堆两部分。
1.小块内部的分配:对于小块内存分配,其内部维护了60个不同大小的分配器(实际源码中看到的是86个),和ptmalloc不同的是,它的每个分配器的大小差是不同的,依此按8字节、16字节、32字节等间隔开。在内存分配的时候,会找到最小符合条件的,比如833字节到1024字节的内存分配请求都会分配一个1024大小的内存块。如果这些分配器的剩余内存不够了,会向中央堆申请一些内存,打碎以后填入对应分配器中。同样,如果中央堆也没内存了,就向中央内存分配器申请内存。
在线程缓存内的60个分配器分别维护了一个大小固定的自由空间链表,直接由这些链表分配内存的时候是不加锁的。但是中央堆是所有线程共享的,在由其分配内存的时候会加自旋锁(spin lock)。
2.大内存的分配:对于大内存分配(大于8个分页, 即32K),tcmalloc直接在中央堆里分配。中央堆的内存管理是以分页为单位的,同样按大小维护了256个空闲空间链表,前255个分别是1个分页、2个分页到255个分页的空闲空间,最后一个是更多分页的小的空间。这里的空间如果不够用,就会直接从系统申请了。
3.ptmalloc与tcmalloc的不足:都是针对小内存分配和管理;对大块内存还是直接用了系统调用。应该尽量避免大内存的malloc/new、free/delete操作。频繁分配小内存,例如:对bool、int、short进行new的时候,造成内存浪费。
三、Jemalloc: jemalloc 是由 Jason Evans 在 FreeBSD 项目中引入的新一代内存分配器。它是一个通用的malloc实现,侧重于减少内存碎片和提升高并发场景下内存的分配效率,其目标是能够替代 malloc。下面是Jemalloc的两个重要部分:
1.arena:arena 是 jemalloc 最重要的部分,内存由一定数量的 arenas 负责管理。每个用户线程都会被绑定到一个 arena 上,线程采用 round-robin 轮询的方式选择可用的 arena 进行内存分配,为了减少线程之间的锁竞争,默认每个 CPU 会分配 4 个 arena,各个 arena 所管理的内存相互独立。
struct arena_s {
atomic_u_t nthreads[2];
tsdn_t *last_thd;
arena_stats_t stats; // arena的状态
ql_head(tcache_t) tcache_ql;
ql_head(cache_bin_array_descriptor_t) cache_bin_array_descriptor_ql;
malloc_mutex_t tcache_ql_mtx;
prof_accum_t prof_accum;
uint64_t prof_accumbytes;
atomic_zu_t offset_state;
atomic_zu_t extent_sn_next; // extent的序列号生成器状态
atomic_u_t dss_prec;
atomic_zu_t nactive; // 激活的extents的page数量
extent_list_t large; // 存放 large extent 的 extents
malloc_mutex_t large_mtx; // large extent的锁
extents_t extents_dirty; // 刚被释放后空闲 extent 位于的地方
extents_t extents_muzzy; // extents_dirty 进行 lazy purge 后位于的地方,dirty -> muzzy
extents_t extents_retained; // extents_muzzy 进行 decommit 或 force purge 后 extent 位于的地方,muzzy -> retained
arena_decay_t decay_dirty; // dirty --> muzzy
arena_decay_t decay_muzzy; // muzzy --> retained
pszind_t extent_grow_next;
pszind_t retain_grow_limit;
malloc_mutex_t extent_grow_mtx;
extent_tree_t extent_avail; // heap,存放可用的 extent 元数据
malloc_mutex_t extent_avail_mtx; // extent_avail的锁
bin_t bins[NBINS]; // 所有用于分配小内存的 bin
base_t *base; // 用于分配元数据的 base
nstime_t create_time; // 创建时间
};
2.extent:管理 jemalloc 内存块(即用于用户分配的内存)的结构,每一个内存块大小可以是 N * page_size(4KB)(N >= 1)。每个 extent 有一个序列号(serial number)。一个 extent 可以用来分配一次 large_class 的内存申请,但可以用来分配多次 small_class 的内存申请。
struct extent_s {
uint64_t e_bits; // 8字节长,记录多种信息
void *e_addr; // 管理的内存块的起始地址
union {
size_t e_size_esn; // extent和序列号的大小
size_t e_bsize; // 基本extent的大小
};
union {
/*
* S位图,当此 extent 用于分配 small_class 内存时,用来记录这个 extent 的分配情况,
* 此时每个 extent 的内的小内存称为 region
*/
arena_slab_data_t e_slab_data;
atomic_p_t e_prof_tctx; // 一个计数器,用于large object
};
}