Go 内存分配器原理剖析:mcache、mcentral 与 mheap 的协作机制

Go 语言以高并发和低延迟著称,这背后离不开其精心设计的内存分配器。Go 的内存分配器借鉴了 TCMalloc(Thread-Caching Malloc)的思想,采用多级缓存架构,将内存管理划分为 mcachemcentralmheap 三个核心组件。它们各司其职、层层配合,既保证了分配速度,又兼顾了内存利用率。本文将深入剖析这三者的职责、数据结构以及协作机制。

一、整体架构概览

Go 内存分配器采用 三级缓存 模型:

  • mcache:每个 P(Processor,逻辑处理器)私有的缓存,用于无锁快速分配小对象。
  • mcentral:全局的中央缓存,按 size class 管理 span,供所有 mcache 共享。
  • mheap:堆内存的顶层管理者,负责向操作系统申请内存,并以页(page,8KB)为单位进行管理。

分配流程大致如下:当 goroutine 需要分配小对象时,首先访问当前 P 绑定的 mcache;若 mcache 中对应 size class 的空闲 span 耗尽,则向 mcentral 申请;若 mcentral 也资源不足,则向 mheap 申请新的 span;mheap 若内存不够,则通过系统调用向操作系统申请内存。

这种设计将大部分分配请求拦截在 mcache 层,避免了全局锁竞争,从而实现了高效的内存分配。

二、mcache:每个 P 的私有缓存

mcache 是绑定在 P 上的私有结构,因此访问它不需要加锁。它的核心是一个 alloc 数组,长度为 67(对应 67 种 size class),每个元素指向一个 mspan。mspan 是 Go 内存管理的基本单位,代表一组连续的页。

type mcache struct {
    tiny             uintptr
    tinyoffset       uintptr
    alloc            [numSizeClasses]*mspan
    // ...
}

每个 mspan 内部维护了一个空闲对象链表(freelist)。当分配一个小对象时,mcache 根据对象大小找到对应的 size class,然后从对应的 mspan 的 freelist 中取出一个空闲 slot 返回。整个过程仅涉及指针操作,速度极快。

mcache 还包含一个 tiny allocator,专门用于分配小于 16 字节且不含指针的小对象。它将这些微小对象合并到同一个 16 字节的块中,以减少内存碎片。

当 mcache 中某个 size class 的 mspan 空闲列表为空时,它会向 mcentral 发起申请,请求一个新的 mspan。

三、mcentral:全局中央缓存

mcentral 是所有 mcache 共享的全局结构,按 size class 组织。每个 size class 对应一个 mcentral,内部维护两个 mspan 链表:

  • nonempty:还有空闲 slot 的 mspan 链表。
  • empty:没有空闲 slot 的 mspan 链表。
type mcentral struct {
    spanclass   spanClass
    nonempty    mSpanList
    empty       mSpanList
    // ...
}

当 mcache 向 mcentral 申请 mspan 时,mcentral 会从 nonempty 链表中取出一个 mspan 返回。如果 nonempty 为空,则从 empty 链表中查找是否有可以回收的 mspan(例如所有对象都已释放),或者向 mheap 申请新的 mspan。

当 mcache 释放对象时,如果 mspan 变为完全空闲,它会被归还给 mcentral,放入 empty 链表。mcentral 会定期扫描 empty 链表,将完全空闲的 mspan 归还给 mheap,以便内存复用或返还操作系统。

由于 mcentral 是全局的,访问它需要加锁(mcentral.lock)。但得益于 mcache 的缓冲作用,大部分分配请求不会到达这一层,锁竞争被大幅降低。

四、mheap:堆内存的顶层管理者

mheap 是 Go 内存分配器的最高层,负责管理整个堆内存。它主要包含以下结构:

  • free:一个按 size class 组织的空闲 span 数组,用于管理不同大小的空闲 span。
  • central:一个 mcentral 数组,长度为 67,对应所有 size class。
  • arenas:堆内存的元数据,记录每个 arena(64MB)的使用情况。
  • lock:全局锁,保护 mheap 的并发访问。
type mheap struct {
    lock      mutex
    free      [numSpanClasses]mSpanList
    central   [numSpanClasses]mcentral
    arenas    [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    // ...
}

当 mcentral 向 mheap 申请新的 mspan 时,mheap 会执行以下步骤:

  1. 根据请求的 size class 计算所需的页数。
  2. free 数组中查找合适的空闲 span。如果找到,则将其切分并返回。
  3. 如果找不到,则向操作系统申请新的内存(通过 sysAlloc),通常以 64MB 的 arena 为单位。
  4. 将新申请的内存切分为 span,并初始化后返回给 mcentral。

mheap 还负责垃圾回收(GC)后的内存回收。当 GC 标记清除后,完全空闲的 span 会被归还给 mheap,进而可能通过 sysFree 返还给操作系统。

五、协作机制:一次分配的生命周期

让我们以分配一个 32 字节的对象为例,梳理整个协作流程:

  1. mcache 层:goroutine 根据当前 P 找到 mcache,定位到 32 字节对应的 size class(例如 class 5)。如果该 mspan 的 freelist 非空,直接取出一个 slot 返回。
  2. mcentral 层:如果 freelist 为空,mcache 向对应 size class 的 mcentral 申请一个新的 mspan。mcentral 加锁,从 nonempty 链表中取出一个 mspan 返回,并更新链表。
  3. mheap 层:如果 mcentral 的 nonempty 和 empty 都为空,则向 mheap 申请。mheap 加锁,从 free 数组中查找合适的空闲 span。如果找到,切分后返回;否则向操作系统申请新的 arena,再切分返回。
  4. 返回路径:新 mspan 被返回给 mcentral,mcentral 将其放入 nonempty 链表,并返回给 mcache。mcache 将其挂载到 alloc 数组,然后从中分配对象返回给 goroutine。

释放对象时,流程相反:对象被放回 mspan 的 freelist,当 mspan 完全空闲时,从 mcache 归还给 mcentral,最终可能归还给 mheap。

六、总结

Go 内存分配器通过 mcache、mcentral 和 mheap 的三级协作,实现了高效、可扩展的内存管理:

  • mcache 提供了无锁的快速分配路径,适应高并发场景。
  • mcentral 作为全局缓冲,平衡了多个 P 之间的内存需求。
  • mheap 负责与操作系统交互,管理堆内存的申请与释放。

这种分层设计既减少了锁竞争,又提高了内存利用率,是 Go 运行时高性能的重要基石。理解这三者的协作机制,有助于我们编写更高效的 Go 程序,并在性能调优时做出更明智的决策。

未经允许不得转载:任鹏个人博客 » Go 内存分配器原理剖析:mcache、mcentral 与 mheap 的协作机制

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏