Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.3 内存分配与回收

内存分配与回收是内存管理的核心功能,操作系统需要高效地管理物理内存,为进程分配和回收内存空间,同时标准库也会在用户态提供更细粒度的内存分配,满足程序的动态内存需求。

操作系统的内存管理

操作系统的内存管理分为两个层面:

  1. 物理内存管理:管理整个系统的物理页,分配给各个进程
  2. 虚拟内存管理:管理每个进程的虚拟地址空间,将虚拟页映射到物理页

物理内存分配

操作系统需要管理所有的物理页,为进程分配和回收物理页,常用的物理内存分配算法:

1. 伙伴系统(Buddy System)

伙伴系统是Linux内核使用的物理页分配算法:

  • 原理:把物理内存按2的幂次方大小划分成多个块,每个块大小是4KB、8KB、16KB…直到最大的块大小
  • 当需要分配n个页时,找到大于等于n的最小的2的幂次方的块分配
  • 如果没有合适的块,就把更大的块分裂成两个相等的伙伴块,直到得到需要的大小
  • 回收时,检查回收的块的伙伴块是否也是空闲的,如果是就合并成更大的块
  • 优点:实现简单,合并快速,减少外部碎片
  • 缺点:内部碎片,比如需要5个页的话要分配8个页,浪费3个页的空间

2. Slab分配器

伙伴系统是以页为单位分配的,但操作系统经常需要分配小于页大小的内核对象(比如进程描述符、inode等),Slab分配器就是用来分配小对象的:

  • 原理:为每种类型的内核对象创建一个缓存,缓存中包含多个Slab,每个Slab由一个或多个连续的物理页组成,里面存放固定大小的对象
  • 分配时直接从对应的缓存中分配空闲对象,回收时放回缓存
  • 优点:分配小对象速度极快,不会产生内部碎片,缓存常用的对象,避免重复初始化
  • Linux内核中使用的是改进的Slub分配器,性能更好,更节省内存

进程虚拟地址空间布局

每个进程的虚拟地址空间是独立的,32位Linux系统的典型布局:

┌───────────────────┐ 0xFFFFFFFF
│       内核空间     │  1GB,内核使用,所有进程共享
├───────────────────┤ 0xC0000000
│       栈区         │  从高地址向低地址增长,存储函数调用栈、局部变量
├───────────────────┤
│    内存映射区      │  动态库、文件映射、mmap分配的内存
├───────────────────┤
│       堆区         │  从低地址向高地址增长,动态分配的内存(malloc/new)
├───────────────────┤
│    数据段(BSS)   │  未初始化的全局变量和静态变量,初始化为0
├───────────────────┤
│    数据段(Data)  │  已初始化的全局变量和静态变量
├───────────────────┤
│    代码段(Text)  │  程序的可执行代码、只读常量
└───────────────────┘ 0x00000000

64位系统的虚拟地址空间更大,布局类似,但各个段的范围更大。

用户态内存分配:malloc实现

我们在程序中调用的malloc()/free()是C标准库提供的函数,属于用户态的内存分配,不是系统调用。标准库的内存分配器会提前向操作系统申请大块的内存,然后切分成小块分配给程序,减少系统调用的开销。

内存分配器的设计目标

  1. 高性能:分配和释放操作要尽可能快
  2. 低碎片:减少内存碎片,提高内存利用率
  3. 通用性:支持各种大小的内存分配请求
  4. 可移植性:在不同的操作系统和硬件平台都能运行
  5. 线程安全:支持多线程环境下的并发分配

常见的内存分配器

1. ptmalloc

ptmalloc是GNU C库(glibc)的默认内存分配器:

  • 原理:基于Doug Lea的dlmalloc,支持多线程
  • 使用分箱(Binning)技术,把不同大小的内存块放到不同的链表中,分配时直接从对应大小的链表中取
  • 每个线程有自己的分配区(Arena),减少锁竞争
  • 优点:稳定、兼容性好,是Linux的默认实现
  • 缺点:内存碎片较多,多线程下性能一般,释放的内存不一定会还给操作系统

2. tcmalloc(Thread-Caching Malloc)

tcmalloc是Google开发的内存分配器,是gperftools的一部分:

  • 核心优化:每个线程有自己的线程本地缓存,小对象分配直接在本地缓存中进行,不需要加锁,性能极高
  • 大对象使用Central Cache分配,多个线程共享,使用页级分配
  • 优点:多线程下性能比ptmalloc高很多,内存碎片少,释放的内存会还给操作系统
  • 缺点:比ptmalloc多占用一点额外内存
  • 很多高性能应用(比如Chrome、Redis)都使用tcmalloc代替默认的ptmalloc

3. jemalloc

jemalloc是Jason Evans开发的内存分配器,最早用于FreeBSD,现在是Firefox、Rust、Facebook的默认分配器:

  • 核心优化:基于多层级的缓存设计,支持多核扩展,低碎片
  • 使用大小类和范围分配,每个CPU有自己的缓存,并发性能很好
  • 内建内存 profiling 功能,方便排查内存问题
  • 优点:性能和tcmalloc相当甚至更好,在多线程大内存分配场景下表现优异,内存利用率高
  • 现在很多高并发服务都使用jemalloc来提升性能

分配器性能对比

分配器单线程性能多线程性能内存碎片额外内存开销
ptmalloc中等一般
tcmalloc中等
jemalloc很高很低中等

选择建议

  • 默认情况下用系统默认的ptmalloc就可以
  • 高并发多线程服务,建议换成jemalloc或tcmalloc,能带来明显的性能提升
  • 内存受限的嵌入式场景,考虑更轻量的分配器比如musl libc的malloc

malloc的底层实现

malloc分配内存主要通过两个系统调用:

  1. brk():扩展堆区的边界,适合分配较小的内存
  2. mmap():在内存映射区分配一块匿名内存,适合分配较大的内存(通常大于128KB)

free释放内存时:

  • 如果是brk分配的小内存,放回空闲链表,不一定会还给操作系统,会被缓存起来下次分配使用
  • 如果是mmap分配的大内存,调用munmap直接还给操作系统
  • 这就是为什么有时候程序占用的内存不会随着free而下降的原因,缓存的内存可以复用,减少系统调用

内存回收

操作系统的内存回收

操作系统会在内存不足时回收内存:

  1. 回收页缓存:回收不常用的文件缓存页,这些页可以从磁盘重新读取,不需要写回(如果是干净的)
  2. 交换出匿名页:把不常用的匿名内存页写到交换分区,释放物理内存
  3. OOM Killer:如果内存严重不足,会杀死占用内存多的进程,释放内存

垃圾回收(GC)

对于Java、Python、Go、JavaScript等带自动垃圾回收的语言,不需要手动调用free释放内存,垃圾回收器会自动回收不再使用的内存:

  • 原理:跟踪所有内存对象的引用,找出不再被引用的对象,自动回收它们占用的内存
  • 常见的GC算法:标记清除、标记复制、标记整理、分代回收、引用计数等
  • 优点:减轻程序员的负担,减少内存泄漏和野指针问题
  • 缺点:有运行时开销,GC停顿会影响响应时间,需要调优

手动内存管理

C/C++等语言需要手动管理内存,调用malloc/new分配,free/delete释放:

  • 优点:没有GC开销,内存使用完全可控,性能更高
  • 缺点:容易出现内存泄漏、野指针、双重释放等问题,对程序员要求高

常见问题

为什么malloc分配的小内存free后,进程的内存占用没有下降?

因为ptmalloc等分配器会把释放的小内存缓存起来,留给后续的malloc使用,不会立即还给操作系统,这样可以避免频繁的系统调用,提升性能。如果很长时间不使用,或者分配了大内存,才会还给操作系统。

什么是内存碎片?

内存碎片分为两种:

  1. 外部碎片:总空闲内存足够,但都是不连续的小块,无法分配连续的大块内存
  2. 内部碎片:分配的内存比实际需要的大,多余的部分浪费了

好的内存分配器会尽量减少内存碎片,提升内存利用率。

如何选择内存分配器?

  • 普通应用:默认的ptmalloc足够
  • 高并发多线程服务:jemalloc或tcmalloc,性能更好
  • 嵌入式系统:轻量级分配器,减少内存开销
  • 需要调试内存问题:使用带调试功能的分配器,比如AddressSanitizer

思考问题

  1. 操作系统的伙伴系统分配算法有什么优缺点?
  2. 标准库的malloc为什么不直接向操作系统申请内存,而是提前申请大块内存再切分?
  3. tcmalloc和jemalloc相比默认的ptmalloc有什么优势?为什么高并发场景下性能更好?
  4. 自动垃圾回收和手动内存管理各有什么优缺点?分别适合什么场景?