内存分配器
用户态
ptmalloc:是Glibc(GNU C Library)中的内存分配器,它基于Doug Lea的malloc(dlmalloc)实现,主要用于用户态的内存分配。ptmalloc在Glibc中负责管理堆内存,包括
malloc()
、calloc()
、realloc()
和free()
等函数的实现- 优点:
- 广泛兼容:几乎在所有Linux系统中默认可用,无需额外安装。
- 支持多种分配策略,对于小对象分配效率较高。
- 针对多线程环境进行了部分优化,尽管不是专门为此设计。
- 缺点:
- 在高并发或多线程环境下性能不如专门设计的内存分配器,如jemalloc和tcmalloc。
- 对于大对象和长时间存活的对象,ptmalloc可能产生较大的内存碎片。
- 分配器的锁定策略可能导致热点区域的竞争激烈,影响性能。
- malloc: 是C语言标准库提供的动态内存分配函数,用于在程序运行时从堆上分配一块指定大小的连续内存区域
void* malloc(size_t size);
它接收一个参数,即所需内存块的大小(以字节为单位),并返回一个指向分配内存区域的指针。如果内存分配失败,则返回
NULL
。- 优点:
- 灵活性:可以在运行时动态决定内存分配的大小。
- 通用性:几乎所有C/C++环境均支持此函数。
- 缺点:
- 不初始化内存:分配的内存区域不会被初始化,其内容是不确定的。
- 不提供类型安全:返回的是无类型的指针,需要显式类型转换。
- 不支持构造函数和析构函数:对于类类型对象,单纯使用
malloc()
不能正确初始化对象。
- calloc: 是C语言标准库中的另一个动态内存分配函数,除了分配内存之外,还会初始化分配的内存区域。
void* calloc(size_t num, size_t size);
它接收两个参数,分别为元素的数量和单个元素的大小,总共分配的内存大小为
num * size
,并把分配到的内存初始化为0。- 优点:
- 初始化内存:分配的内存会被清零,适合需要初始化为0的场景。
- 一次性分配多个对象时简便快捷。
- 缺点:
- 同
malloc()
一样不具备类型安全,不支持构造函数和析构函数。
- 同
- realloc: 动态地改变先前由
malloc()
或calloc()
分配的内存块的大小
void* realloc(void* ptr, size_t new_size);
它接收两个参数,一个是已分配内存区域的指针,另一个是新的大小。如果成功,返回指向新大小内存区域的指针;如果失败,则返回
NULL
,原来的内存区域保持不变。- 优点:
- 动态调整:根据程序运行时的需求,可以扩展或收缩已分配的内存区域。
- 缺点:
- 可能导致内存碎片:频繁调整内存大小可能会加剧内存碎片问题。
- 失败时原有内存可能丢失:如果
realloc()
失败,原有的内存区域仍保留,但程序可能无法得知这一点。
- std::allocator: 在C++标准库中,
std::allocator
是STL容器(如std::vector
、std::list
等)默认使用的内存分配器模板类,它提供了一种类型安全的方式来分配和释放内存。
template <class T> class allocator { // 成员函数和类型声明... };
使用
std::allocator
分配内存时,通常间接通过STL容器完成,也可以直接实例化并调用成员函数来分配和释放内存。- 优点:
- 类型安全:模板化的设计使其能够处理任意类型,且分配的内存与类型关联。
- 构造和析构:对于类类型对象,
std::allocator
会在分配内存的同时调用对象的构造函数,释放内存时调用析构函数。
- 缺点:
- 默认行为较为基础:
std::allocator
的默认实现基于operator new
和operator delete
,在某些特殊场景下(如高性能、低延迟或特定内存管理需求)可能不够优化。 - 对于复杂内存管理策略,可能需要自定义分配器。
- 默认行为较为基础:
- 总结
malloc()
和calloc()
是C语言层面的底层内存分配函数,灵活性强但安全性较低。realloc()
用于调整已分配内存的大小,方便动态扩展或缩小内存。std::allocator
是C++ STL中面向对象的内存分配器,提供类型安全和构造函数/析构函数的支持,适用于容器类的内存管理。std::allocator
是高层抽象,而malloc()
、calloc()
和realloc()
是更低层次的内存管理函数,直接操作内存。在C++中,除非有特殊需求,一般推荐使用STL容器和配套的内存管理设施
- 优点:
jemalloc: 是是一种高性能、低碎片的内存分配器,最初由Jason Evans开发,现在被广泛应用于众多开源项目和商业产品中,如NetBSD、Mozilla Firefox、Facebook的HipHop Virtual Machine (HHVM)、Elasticsearch以及Redis等
- 优点:
- 多线程优化:jemalloc使用了线程局部缓存(Thread-Local Cache, TLABs),每个线程有自己的内存缓存区域,从而减少了多线程环境下的锁竞争,提升了内存分配和释放的速度。
- 内存区域分类:jemalloc将内存划分为多个大小类别(内存池),对不同大小的对象有不同的分配策略,有助于减少内存碎片。
- 合并相邻内存区域:jemalloc能够有效地合并相邻的空闲内存区域,以维持较高的内存利用率。
- 异步回收和重分配:jemalloc支持异步内存回收,可以避免在内存紧张时立即回收内存带来的性能损失,并且在一定程度上支持大块内存的重分配。
- 多层分配策略:jemalloc不仅在小对象分配上有优化,在大对象分配上也有相应的策略,能更好地适应不同大小对象的分配需求。
- 缺点:
- 需要额外集成到项目中,不像ptmalloc那样是标准库的一部分。
- 对于非常小的对象,jemalloc可能不如经过高度优化的标准库分配器快。
- 优点:
tcmalloc:全称Thread-Caching Malloc,是由Google开发的一种内存分配器,专为多线程环境设计,同样致力于减少内存碎片和提高分配效率,特别关注于减少锁竞争和提升分配速度。
- 优点:
- 线程缓存:tcmalloc也为每个线程分配了本地缓存,小对象的分配和回收优先在本地缓存中进行,极大地减少了锁竞争。
- 快速分配:tcmalloc使用了大小类别的内存池技术,预先分配和管理不同大小的内存块,对于常见的小对象分配非常快速。
- 堆碎片控制:通过精细化的内存管理策略,tcmalloc能在一定程度上减少内存碎片的产生,提高了内存利用率。
- 透明替换glibc的malloc:tcmalloc可以无缝替代glibc中的malloc、calloc、realloc和free等函数,开发者无需大幅改动代码就能获得性能提升。
- 内存统计和诊断:tcmalloc提供了内存使用情况的统计信息,便于开发者分析和优化程序内存使用。
- 缺点:
- 同样需要单独集成到项目中,非默认组件。
- 对于特别大的对象,其表现可能不如专门针对大对象优化的分配器。
- 有可能增加线程栈的消耗,尤其是在高并发场景下。
jemalloc和tcmalloc都是为了改进传统内存分配器(如ptmalloc,即glibc中的malloc实现)在多线程环境下的性能问题而设计的。二者在很多设计理念和技术细节上相似,但在具体的实现和优化策略上略有差异,适用于不同的场景和需求。在实际应用中,根据项目的特点和性能需求,开发者可以选择最适合的内存分配器。
- 优点:
系统调用
brk/sbrk
brk() 是一个Unix/Linux系统调用,用于调整进程的数据段大小,即扩大或缩小进程的堆空间。调用 brk() 会改变进程的程序_break(program break),也就是堆的顶部位置。
void *brk(void *addr);
当传入参数为空指针时,brk() 会返回当前的程序断点;当传入一个新的地址时,它会尝试将程序断点移动到那个地址,如果成功则返回0,否则返回-1。
sbrk() 是brk()的一个库函数封装,提供了与brk()类似的接口,但更容易使用
void *sbrk(intptr_t increment);
sbrk() 接受一个增量值,调用时会将程序断点向前或向后移动指定的字节数,然后返回新的程序断点地址.
- 优点:简单易用,适用于传统的堆内存分配。
- 缺点:由于brk()和sbrk()只能在进程的现有堆区域内扩展或收缩内存,它们可能难以应对复杂的内存布局需求,且可能引入内存碎片。
- 区别:brk()需要传入绝对地址,而sbrk()只需传入增量值
mmap/unmmap
mmap() 是一个Unix/Linux系统调用,用于将文件或者匿名内存映射到进程的地址空间中。它能够直接映射物理内存,不仅可以用于堆,还可以用于堆外内存分配。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap() 可以创建一个新的内存映射区域,或者扩展、修改已存在的映射区域。
munmap() 用于解除由mmap()创建的内存映射
int munmap(void *addr, size_t length);
- 优点:mmap()可以映射大块连续内存,而且可以映射文件,支持虚拟内存管理,有利于大对象分配和交换空间的使用。对于减少内存碎片、支持跨进程共享内存等方面有优势。
- 缺点:mmap()创建的映射区域可能会占用交换空间,如果频繁映射和解除映射可能会带来一定的性能开销。
- 区别:与brk()/sbrk()相比,mmap()的内存分配更灵活,不受限于进程堆的大小,可以映射的内存范围更大,而且能够映射磁盘文件,实现内存映射文件I/O。
内核态
kmalloc:
- kmalloc() 是Linux内核的一种内存分配函数,主要用于分配连续的物理内存块,并且返回的是可以直接寻址的物理地址,分配的内存位于内核的物理内存区域(ZONE_NORMAL、ZONE_DMA等)
优点:
- 保证物理内存连续:这对于需要连续内存的数据结构(如设备驱动中的缓冲区)或者直接内存访问(DMA)非常重要,因为DMA控制器只能处理物理连续的内存,返回的是可以直接寻址的内核虚拟地址,配的内存通常来自内核预留的物理内存区域。
- 速度快:由于kmalloc分配的是物理连续的内存,对于CPU和某些硬件访问(如DMA操作)更为高效。
缺点:
- 内存限制:由于物理内存资源有限,kmalloc能分配的最大内存块大小受到限制,且分配的大块连续内存容易造成碎片
- 难以满足大内存需求:对于需要分配大量连续内存的请求,kmalloc可能无法满足
vmalloc:
- vmalloc() 也是Linux内核中的内存分配函数,但它分配的是虚拟地址连续的内存,不保证物理内存连续。vmalloc分配的内存位于内核虚拟地址空间的高端部分,这部分地址与物理内存的映射关系更加灵活
优点:
- 可分配大内存:vmalloc可以分配远大于kmalloc所能分配的内存块,不受物理内存连续性的限制
- 减少碎片:vmalloc通过页表映射机制,能够在物理内存碎片严重的情况下依然能提供大块的连续虚拟地址空间
缺点:
- 性能损耗:由于涉及到虚拟地址到物理地址的转换,访问vmalloc分配的内存需要TLB(Translation Lookaside Buffer)的支持,对于频繁访问或需要物理连续内存的场景,性能相比kmalloc有所下降
- 不支持DMA:vmalloc分配的内存不适合直接用于DMA操作,因为DMA控制器通常要求物理地址连续
kmalloc适用于需要小到中等大小且物理连续内存的场景,尤其是那些涉及直接内存访问操作的设备驱动。而vmalloc则更适合分配大块的、不一定要求物理连续的内存,用于内核自身的数据结构或者内存映射等情况。