Go 语言的内存分配策略是怎样的?
Go 语言的内存分配策略是其高性能并发的关键之一。简单来说,Go 的内存分配器是基于 Google 的 TCMalloc (Thread-Caching Malloc) 算法改进而来的。
其核心思想可以概括为:多级缓存、无锁分配(针对小对象)、隔离大小。
以下是 Go 内存分配策略的详细解析:
1. 核心设计原则
- 内存自主管理:Go 运行时(Runtime)向操作系统申请一大块内存(Arena),然后自己管理,减少系统调用(Syscall)的开销。
- 分级分配:根据对象的大小,使用不同的分配策略(微小对象、小对象、大对象)。
- 本地缓存(Thread Cache):为每个逻辑处理器(P)提供本地缓存,绝大多数分配不需要加锁,极大地提高了并发性能。
2. 内存分配的层级结构
Go 的内存分配器由三级组件组成,类似于 CPU 的 L1/L2/L3 缓存架构:
A. mcache (线程/处理器缓存)
- 位置:绑定在 P (Processor) 上。
- 特点:无锁。因为每个 P 在同一时间只能运行一个 Goroutine,所以访问
mcache不需要加锁,速度极快。 - 作用:维护了不同规格(Size Class)的
mspan列表,用于快速分配小对象。
B. mcentral (中心缓存)
- 位置:全局共享。
- 特点:需要加锁。
- 作用:当
mcache中的内存用完时,会向mcentral申请。mcentral收集了各种规格的mspan,负责在不同 P 之间平衡内存使用。
C. mheap (堆内存)
- 位置:全局唯一。
- 特点:需要加锁(粒度较大)。
- 作用:内存管理的源头。当
mcentral也不够用时,会向mheap申请。mheap负责向操作系统申请大块内存(Arena),并将其切割成mspan管理。
3. 内存管理单元:mspan
Go 将内存页(Page,通常为 8KB)组合成 mspan,这是内存管理的基本单元。
- Size Class(规格):Go 将内存划分为约 67 种不同的规格(例如 8B, 16B, 32B ... 32KB)。
- 每个
mspan只能存储特定规格的对象。例如,一个规格为 16B 的mspan会被切分成许多个 16B 的小块。 - 好处:这种固定规格的分配方式极大地减少了内存碎片。
4. 分配流程(按对象大小分类)
Go 根据对象的大小(Size)采取三种不同的分配策略:
A. 微小对象 (Tiny Objects) < 16B
- 策略:合并分配。
- 过程:为了节省内存,Go 会将多个微小对象(如
bool,int8)拼凑在一个 16B 的内存块中。这类似于“拼车”。 - 目的:减少碎片,提高缓存局部性。
B. 小对象 (Small Objects) 16B ~ 32KB
- 策略:多级缓存分配。
- 过程:
- 计算规格:确定对象属于哪个 Size Class(例如申请 20B,会匹配到 32B 的规格)。
- 查
mcache:去当前 P 的mcache中找对应规格的mspan。如果有空闲块,直接分配(无锁,极快)。 - 查
mcentral:如果mcache满了,向mcentral申请一个新的mspan(需要加锁)。拿到后放入mcache继续分配。 - 查
mheap:如果mcentral也空了,向mheap申请(需要加锁)。 - 问 OS:如果
mheap也不够,向操作系统申请新的虚拟内存。
C. 大对象 (Large Objects) > 32KB
- 策略:直接分配。
- 过程:跳过
mcache和mcentral,直接向mheap申请分配一组连续的页(mspan)。 - 原因:大对象数量少,但占用空间大,不适合放在缓存中,否则容易频繁触发缓存淘汰。
5. 逃逸分析 (Escape Analysis)
在讨论堆分配之前,必须提到逃逸分析。并不是所有变量都会分配在堆上。
- 栈 (Stack):函数内部的局部变量,生命周期随函数结束而结束的,通常分配在栈上。栈分配极其廉价(只是移动栈指针)。
- 堆 (Heap):如果编译器发现变量在函数返回后仍被引用(例如返回了指针),或者变量太大(栈放不下),或者大小不确定,该变量就会“逃逸”到堆上。
Go 的策略是:能分配在栈上,绝不分配在堆上。 只有逃逸的变量才走上述的 mcache/mcentral/mheap 流程。
6. 总结图解
plaintext
Goroutine 申请内存
|
v
对象大小?
/ \ \
<16B 16B~32KB >32KB
| | |
Tiny分配 | |
(组合包) | |
| v v
+-----> mcache (本地无锁)
| 没货?
v
mcentral (共享,有锁)
| 没货?
v
mheap (全局,有锁) <------- 大对象直接走这里
| 没货?
v
OS (操作系统)
总结 Go 内存分配的优势
- 速度快:通过
mcache实现小对象的无锁分配,覆盖了绝大多数场景。 - 碎片少:通过
mspan的固定规格(Size Class)管理,避免了外部碎片。 - 利用率高:Tiny 对象合并分配,节省空间。
- GC 友好:内存块的布局和元数据设计(Bitmap)有助于垃圾回收器快速扫描。