每日一Go-39、Go 内存分配器深度拆解--Arena /Span / MSpan / 大对象 / 小对象

本文从最高层的Arena到中层的Span/MSpan,再到线程级别的mcache逐层拆解Go的内存分配器。这是Go Runtime最核心的组件之一,与GC、goroutine调度器一同构成三大根基。

一、Go的Heap结构总览

Go的Heap是分层设计的:

bash 复制代码
┌───────────────────────────────────┐
│               Arena               │  ------ 模块化的巨大地址空间
├───────────────────────────────────┤
│               Span                │  ------ 内存管理的基本单元 (8KB ~ 数MB)
├───────────────────────────────────┤
│           SpanClass (size class)  │  ------ 67 种固定大小桶
├───────────────────────────────────┤
│               mcentral            │  ------ 每种 size class 的共享池
├───────────────────────────────────┤
│               mcache              │  ------ 每个 P 的本地缓存(无锁)
└───────────────────────────────────┘

最终分配路径是:

bash 复制代码
new(T)
 → mallocgc()
   → mcache
     → mcentral
       → mheap
         → arena

二、Arena是Go Heap的地基

Arena 不是一次性全部分配,而是 按需向操作系统 mmap 一大片虚拟地址空间。Go会提前保留一块区域作为未来堆,这块空间被分为:

  • page:最小分配单位8KB

  • bitmap:标记GC信息

  • spans:page到Span的映射

Go的一个Page等于8KB

三、Span(MSpan):Heap的基本管理单元

Span 是内存分配的核心结构,它管理若干个 page:

bash 复制代码
Span = NPAGES * PAGE_SIZE
//举例
NPAGES = 1 → 1 × 8KB = 8KB
NPAGES = 32 → 32 × 8KB = 256KB

Span 是 GC 的根本工作对象:GC 是按 Span 扫描,而不是按对象。

Span 维护的信息包括:

  • 起始地址

  • 有多少对象

  • 空闲对象链表

  • mark 位图

  • 是否为大对象

  • 是否被 mcache 使用

四、Size Class (67种固定桶)

Go 将对象大小划分为 67 种 Size Class:

bash 复制代码
8 bytes
16 bytes
32 bytes
...
32KB

每种 size class 都有一个 spanClass → 管理这一类对象的 span。

为什么固定桶?

  • 降低碎片

  • 允许 mcache 无锁快速分配

  • 提高 locality

如果对象落入某个 size class,则:

bash 复制代码
小对象:落入某个固定尺寸的 Span
大对象:独立一个 Span

五、小对象和大对象的分配逻辑

bash 复制代码
对象 > 32KB → 大对象
32KB 以内 → 小对象

5.1 小对象:走 size class + mcache

流程:

    1. mallocgc 计算对象尺寸 → size class
    1. 尝试从当前 P 的 mcache 中分配
    1. 如果该 mcache 的 span 用完 → 向 mcentral 申请新的 span
    1. mcentral 再从 mheap 获取新的 span

特点:

  • 极快(无锁)

  • 分配只在 mcache 中操作 O(1)

  • 避免争用

六、大对象(>32KB)直接mheap分配

大对象不会走 size class,也不会进入 mcache。

例:

  • 一个 200KB 的切片 buffer → 申请 200KB 大小的 span

  • 一个 1MB 的结构 → 独占一个 span

特点:

  • 更容易导致碎片

  • GC 会更慢扫描这个 span

  • 对象越大,逃逸到堆上的代价越大

七、mcache,每个P的本地分配缓存

mcache 提供每种 size class 的一个 已部分使用的 Span。

每个调度器 P 都有独立 mcache:

bash 复制代码
P0 → mcache0
P1 → mcache1
Pn → mcachen

所以小对象分配:

  • 无锁

  • 只需要在 span 中移动一个指针

  • 极快,大约 2~3ns

当 mcache 用完一个 span:

  • 从 mcentral 拉取新的 span

  • 空 span 返回 mcentral

八、mcentral,所有P的共享Span池

mcentral:

  • 每种 size class 有一个

  • 管理半空 / 半满 span 的双链表

  • 使用锁(但访问频率低)

mcentral 从哪里拿 span?

  • → mheap

  • mheap 不够时

  • → 从 arena 申请 page

九、mheap,真正从操作系统申请内存的地方

mheap 是 Go 的最终分配器:

  • 当 mcentral 需要新 span → mheap.alloc

  • 当 GC 需要扩堆 → mheap.grow

  • 当需要缩堆 → mheap.freeSpan

mheap 的行为:

  • 使用 mmap() 从 OS 申请大块内存

  • 管理 arena 映射

  • 维护 free list

十、mallocgc的内部流程(敲黑板)

bash 复制代码
//伪代码
func mallocgc(size uintptr) {
    if small object {
        span := mcache[sizeClass]
        obj := span.alloc()
        return obj
    }

    // if large
    span := mheap.alloc(size)
    return span.base()
}

真实 mallocgc 会更复杂,包括:

  • write barrier

  • pointer 扫描

  • zeroing new object

  • 调整 heap goal

  • GC assist

十一、常见误区与真相

误区1:Go 每次分配都会加锁 → 错

  • 小对象分配绝大部分来自 mcache(无锁)

误区2:Go heap 会碎片化 → 部分正确

  • 小对象不容易碎,固定 size class

  • 大对象更容易碎片化

误区3:对象逃逸代价不大 → 错

  • 堆上对象会增加 GC 负担

  • 大对象会直接占 span

误区4:大切片重分配很便宜 → 错

  • 可能多次大对象重新分配

  • 可能触发 GC assist

十二、如何写出更高效的Go代码

避免频繁创建大对象

bash 复制代码
//256KB 缓冲区
//大 struct
//巨大 slice

解决办法:使用 sync.Pool

复用对象 ------ 能走栈不要走堆

bash 复制代码
buf := make([]byte, 32*1024) // 刚好 32KB,依然是"小对象"
做好逃逸分析

避免生成大量含指针的小对象

bash 复制代码
 指针对象会给GC巨大的压力

不要频繁创建 map / slice

bash 复制代码
会分配到堆上
会触发rehash和resize
预估容量是关键

Go 内存分配器就像一个巨大仓储物流中心:

小物品用标准货架(small object → size class),大物品直接分配专仓(large object → big span)。

工人(mcache)本地拿货无需排队,大型操作才去中央仓房(mcentral / mheap)。

*源码地址*

1、公众号"Codee君"回复"每日一Go"获取源码

2、pan.baidu.com/s/1B6pgLWfS...


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!

相关推荐
Bug养殖户2 小时前
go语言http解析(二)路由树解析与注册
go
Assby20 小时前
Java开发者学习Go语言:Go开发和Java开发的一些区别
后端·go
zach01271 天前
脑机接口技术的现象学重构:梅洛-庞蒂知觉理论在神经资本主义批判中的再语境化
go
July_101 天前
为什么你的 Go 协程(Gor...
go
王的宝库1 天前
Go 语言基础进阶:指针、init、匿名函数/闭包、defer
开发语言·go
程序员爱钓鱼1 天前
Go文件路径处理完全指南:path/filepath包详解与实战
后端·面试·go
@PHARAOH1 天前
HOW - Kratos 入门实践(二)- 概念学习
前端·微服务·go
ejinxian1 天前
Go语言完整学习规划(2026版)- Part 1
学习·go
捧 花2 天前
Go + Gin 实现 HTTPS 与 WebSocket 实时通信
websocket·golang·https·go·gin