内存分配器

实现了高并发环境下高速、低延迟内存分配,同时降低锁竞争和内存碎片

1 设计原理

分级分配

将内存划分为多级缓存,根据对象大小进行分类处理

  1. 对象分级:微对象(<16B)、小对象(16B~32KB)、大对象(>32KB)三类
  2. 缓存分级:内存管理机构分为线程缓存(mcache)、中心缓存(mcentral)、页堆(mheap)。
  3. 分配策略:分配时,优先从更靠近线程、无锁的本地缓存(mcache,每个P都有)分配,没有时,逐级向上申请。

虚拟内存布局

Go 程序启动时,会向系统申请一大段连续的虚拟内存地址空间(mheap),整个堆划分为一个个 arena 区域,每个 64MB。

地址空间

内存分配器以 page 为单位与 OS 交互,大小为OS页的整数倍(如8KB)以便与操作系统虚拟内存对齐。Go 运行时通过 mheap 结构管理着所有这些虚拟内存页的分配和释放。

  • 栈区:每个goroutine独立栈,初始2KB,无需GC
  • 堆区:运行时管理,包含所有动态分配对象,通过GC回收
  • 其他区域:代码段、数据段

2 内存管理组件

Go 内存分配器的核心是 mcache → mcentral → mheap 三级缓存结构,逐级降低锁粒度。

2.1 内存管理单元:mspan

内存管理的基本单元,代表一段连续的物理页,由 startAddr(起始地址)和 npages(页数)标识。

核心功能

  • spanclass 按 size class 划分为67种,每个 mspan 仅管理同一种 size class 的对象。
  • 通过 allocBits 位图,标记哪些内存块已被分配,快速定位空闲对象槽,实现快速分配
  • gcmarkBits 用于标记内存的回收情况

mspan 不存储用户数据,仅维护元数据(地址、页数、位图)

2.2 线程缓存:mcache

  • 每个 P 本地私有,无锁访问
  • 持有 68*2 个 mspan,只有当用户申请内存时
  • 工作流程:
    • 分配小对象,直接从 mcache 的对应链表中取出空闲槽,速度快
    • 若当前 size class 的 mspan 耗尽,向 mcentral 申请补充(加锁)
    • 释放对象时,优先归还到 mcache,以便快速复用
  • 核心价值:大部分内存分配在本地无锁完成,降低竞争

2.3 中心缓存:mcentral

  • 全局共享的缓存,按照size class 组织,每个size class 对应一把锁(细粒度)
  • 68 个 mcentral 示例,每个实例管理一个 size class 的 mspan 链表,分别存储包含空闲对象和不包含空闲对象的内存管理单元。
  • 工作流程
    • 当 mcache 需要补充 mspan 时,向对应 size class 的 mcentral 申请
    • mcentral 没有时,向 mheap 申请新内存页(全局锁)
  • 关键设计:锁细化到 size class,避免所有分配竞争同一把锁

2.4 页堆:mheap

  • 全局唯一的堆内存管理者,与 OS 交互
  • 核心功能
    • 存储所有 spanclass 对应的 mcentral
    • 向 mcentral 提供新的内存页
    • 周期性将空闲内存归还 OS
  • 大对象分配或缓存耗尽时才访问 mheap,加全局锁

3 内存分配

runtime.mallocgc

微对象 (Micro Object, < 16B)

对于极小且不包含指针的对象,Go 使用 tiny 分配器进行优化。

每个 mcache 都有一个 16 字节的 tiny 内存块。新来的微对象会尝试塞进这个 tiny 块的空闲部分。如果空间不足,才会从 mcache 中申请一个新的 16 字节 mspan 来替换。这极大地减少了内存碎片和分配次数。

小对象 (Small Object, 16B - 32KB)

小对象是 Go 程序中最常见的分配类型。其分配路径如下:

  1. 尝试 mcache: 根据对象大小找到对应的 spanClass,直接从 P 绑定的 mcachemspan 中分配,无锁
  2. mcentral 请求: 若 mcache 中对应的 mspan 已无空闲块,则向全局的 mcentral 加锁申请一个新的 mspan
  3. mheap 请求: 若 mcentral 也无可用的 mspan,则会向 mheap 申请一组新的内存页来生成 mspan
  4. 向 OS 请求: 若 mheap 内存不足,最终会向操作系统申请新的内存。
大对象 (Large Object, > 32KB)

大对象的分配路径最直接:

  1. 直接向 mheap 分配: 不会经过 mcachemcentral
  2. 按页计算: mheap 根据对象大小计算出需要多少页,直接分配一个或多个连续的页,构成一个 mspan 返回。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐