author: djaigo
title: golang-内存分配器
categories:
- golang
date: 2024-01-01 00:00:00
tags: - golang
- 内存管理
- 内存分配器
- GC
推荐阅读:Go Memory Allocator
Golang 内存分配器概述
Go 语言的内存分配器是一个高性能的内存管理系统,它采用了分级分配和多级缓存的设计,能够高效地管理内存分配和回收。
设计目标
- 高性能: 减少内存分配和回收的开销
- 低延迟: 快速响应内存分配请求
- 内存效率: 减少内存碎片和浪费
- 并发安全: 支持多 goroutine 并发分配
核心设计思想
- 分级分配: 根据对象大小使用不同的分配策略
- 多级缓存: 使用线程本地缓存减少锁竞争
- 内存池化: 复用已分配的内存块
- 按需分配: 延迟分配,减少内存占用
核心组件架构
Go 内存分配器由以下核心组件组成:
graph TB
subgraph mheap["mheap (全局堆)"]
subgraph mcentral_group["mcentral (中心缓存)"]
mc1["mcentral (span)"]
mc2["mcentral (span)"]
mc3["mcentral (span)"]
mcn["..."]
end
subgraph arena_group["arena (堆内存区域)"]
s1["span"]
s2["span"]
s3["span"]
s4["span"]
sn["..."]
end
end
mcache["mcache (P本地)"]
mspan["mspan (内存块)"]
mcache --> mheap
mspan --> mheap
组件关系图
flowchart TD
P["Goroutine (P)"]
mcache["mcache
(每个 P 一个,线程本地缓存)"]
mspan1["mspan
(从 mcache 获取)"]
mcentral["mcentral
(全局中心缓存,按大小分类)"]
mspan2["mspan
(管理相同大小的 span)"]
mheap["mheap
(全局堆管理器)"]
arenas["arenas 数组
(两级索引)"]
heapArena["heapArena
(堆内存区域,64MB)"]
memory["实际内存
(页和对象)"]
P --> mcache
mcache --> mspan1
mcache -->|如果 mcache 没有| mcentral
P --> mcentral
mcentral --> mspan2
mcentral -->|如果 mcentral 没有| mheap
mheap --> arenas
arenas --> heapArena
heapArena --> memory
mspan1 --> memory
mspan2 --> memory
核心组件详解
1. mspan (内存块)
mspan 是内存分配的基本单位,代表一块连续的内存区域。
结构定义(简化)
1 | type mspan struct { |
mspan 的作用
- 内存块管理: 管理一块连续的内存区域
- 对象分配: 从 span 中分配固定大小的对象
- 状态跟踪: 跟踪哪些对象已分配,哪些空闲
- GC 支持: 支持垃圾回收的标记和扫描
span 的初始化
1 | // init 初始化 span |
span 的大小分类
Go 将对象按大小分为多个类别(spanclass):
1 | 类别 0: 0-8 字节 |
span 的状态
1 | type mSpanState uint8 |
2. mcache (线程本地缓存)
mcache 是每个 P(Processor)的本地缓存,用于快速分配小对象。
结构定义(简化)
1 | type mcache struct { |
mcache 的特点
- 线程本地: 每个 P 有独立的 mcache,无需加锁
- 快速分配: 直接从 mcache 分配,无需访问全局结构
- 按类缓存: 为每个 spanclass 缓存一个 span
- 自动补充: 当 span 用完时,从 mcentral 获取新的 span
mcache 的分配流程
- 检查 mcache.alloc[spanclass] 是否有空闲对象
- 如果有,直接分配
- 如果没有,从 mcentral 获取新的 span
- 将新 span 放入 mcache,然后分配
mcache 的分配实现
1 | // allocFromCache 从 mcache 分配对象 |
边界情况处理
1. 内存不足处理
1 | // 当内存不足时的处理逻辑 |
2. span 状态转换
1 | // setState 设置 span 状态(需要加锁) |
3. 并发安全处理
1 | // 在多 goroutine 环境下的安全分配 |
3. mcentral (中心缓存)
mcentral 是全局的中心缓存,管理特定大小的 span。在 Go 1.18+ 版本中,mcentral 结构进行了优化,使用 partial 和 full 替代了原来的 nonempty 和 empty,使语义更加清晰。
mcentral 的核心作用:
- 中间层缓存: 作为 mcache 和 mheap 之间的中间层,减少对全局 mheap 的访问频率
- span 池化管理: 维护特定大小类的 span 池,实现 span 的复用和回收
- 状态分类管理:
partial: 存储部分已分配的 span(还有空闲对象可分配)full: 存储完全分配的 span(所有对象都已分配)- 相比旧版本的
nonempty/empty,新的命名更加清晰直观
- 完全无锁设计: 使用无锁的
spanSet结构,所有操作(push/pop)都是原子操作,无需 mutex - 并发性能优化:
- 双 spanSet 设计减少竞争热点
- 支持多个 P 并发访问而不互相阻塞
- 根据 P ID 分散负载(
partial[P.id % 2]),避免缓存行冲突 - 每个大小类独立管理,进一步降低竞争
- 内存回收: 当 span 完全空闲时,可以归还给 mheap,实现内存的动态调整
- 减少碎片: 集中管理同一大小类的 span,提高内存利用率
- 快速分配: 为 mcache 提供快速的 span 补充,避免频繁访问全局 mheap
- 全局共享: 所有 P 共享同一组 mcentral,实现跨 P 的内存复用
结构定义(简化)
1 | type mcentral struct { |
注意: 在最新的 Go 实现中,mcentral 已经移除了 lock 字段,因为 spanSet 是完全无锁的数据结构,不需要额外的锁保护。
spanSet 结构
spanSet 是 Go 1.18+ 引入的无锁数据结构,用于高效管理 span 集合。在最新的实现中,spanSet 使用原子操作实现完全无锁的队列:
1 | type spanSet struct { |
关键改进:
- 原子打包:
headTail是一个atomic.Uint64,将 head 和 tail 打包在一起,保证原子性 - 无锁操作: 所有操作都使用原子操作,完全无锁
- 环形缓冲区:
blocks数组作为环形缓冲区,支持高效的插入和删除 - 内存对齐: 结构体经过精心设计,避免 false sharing
spanSet 的工作原理
flowchart TD
A["spanSet (无锁队列)"]
B["headTail (atomic.Uint64)"]
B1["[32 bits head][32 bits tail]"]
D["blocks 数组
(环形缓冲区)"]
E["spanSetBlock 0"]
F["spanSetBlock 1"]
G["spanSetBlock N"]
A --> B
B --> B1
A --> D
D --> E
D --> F
D --> G
B1 --> B2["head: 下一个要读取的位置"]
B1 --> B3["tail: 下一个要写入的位置"]
style A fill:#ffcccc
style B fill:#ccffcc
style B1 fill:#ccccff
关键特性:
- 完全无锁: 使用
atomic.Uint64打包 head 和 tail,所有操作都是原子操作 - 原子打包: head 和 tail 打包在一个 64 位整数中,保证原子性更新
- 环形缓冲区:
blocks数组作为环形缓冲区,支持高效的插入和删除(O(1)) - 并发安全: 多个 P 可以并发地从不同的
spanSet中获取 span,无需加锁 - 内存局部性: span 连续存储,提高缓存命中率
- 避免 false sharing: 结构体设计避免缓存行冲突
mcentral 的双 spanSet 设计
mcentral 使用两个 spanSet 数组(partial 和 full),每个数组包含 2 个 spanSet:
1 | partial [2]spanSet // 两个 spanSet,用于无锁操作 |
为什么使用两个 spanSet?
- 减少竞争: 两个
spanSet可以并行操作,减少竞争 - 负载均衡: 根据 P 的 ID 选择不同的
spanSet,分散负载 - 性能优化: 避免所有 P 都访问同一个
spanSet造成的热点
mcentral 的作用
- 集中管理: 集中管理特定大小的 span
- 缓存复用: 缓存已分配的 span,供多个 P 复用
- 内存回收: 当 span 完全空闲时,可以归还给 mheap
- 完全无锁: 使用无锁的
spanSet结构,所有操作都是无锁的,无需任何 mutex - 状态分类: 通过
partial和full清晰区分 span 状态 - 高性能并发: 多个 P 可以并发访问,无锁竞争,性能优异
mcentral 的分配流程
flowchart TD
Start["mcache 请求 span"]
Check["检查 mcentral.partial"]
HasPartial["partial 有可用 span?"]
GetPartial["从 partial 获取 span"]
MoveToFull["span 分配完,移到 full"]
CheckFull["检查 mcentral.full"]
HasFull["full 有可用 span?"]
GetFull["从 full 获取 span"]
CheckMheap["从 mheap 分配新 span"]
Return["返回 span 给 mcache"]
Start --> Check
Check --> HasPartial
HasPartial -->|是| GetPartial
HasPartial -->|否| CheckFull
GetPartial --> MoveToFull
MoveToFull --> Return
CheckFull --> HasFull
HasFull -->|是| GetFull
HasFull -->|否| CheckMheap
GetFull --> Return
CheckMheap --> Return
详细步骤:
- 选择 spanSet: 根据当前 P 的 ID 选择
partial[P.id % 2]或full[P.id % 2],实现负载均衡 - 优先从 partial 获取: 尝试从选定的
partialspanSet 中获取有可用对象的 span(完全无锁操作) - 原子操作: 使用
spanSet.pop()原子地获取 span,无需任何锁 - 状态转换: 如果 span 分配完,使用
spanSet.push()原子地将其移到对应的fullspanSet - 回退到 full: 如果
partial为空,尝试从对应的fullspanSet 中获取(可能有些对象被 GC 释放) - 从 mheap 分配: 如果都没有,从
mheap分配新的 span(这里可能需要锁,因为 mheap 有锁) - 返回给 mcache: 将获取的 span 返回给
mcache
关键点:
- mcentral 操作完全无锁: 所有对
partial和fullspanSet 的操作都是无锁的 - 原子操作保证: 使用
atomic.Uint64的 CAS(Compare-And-Swap)操作保证原子性 - 高性能: 无锁设计避免了锁竞争,显著提升了并发性能
spanSet 选择策略:
1 | // 伪代码:选择 spanSet |
这样可以确保:
- 不同的 P 访问不同的
spanSet,减少竞争 - 负载分散,避免热点
- 保持无锁操作的高性能
spanSet 的核心操作
spanSet 提供两个核心操作,都是完全无锁的:
1 | // spanSet 常量定义 |
关键点:
- 原子读取: 使用
Load()原子地读取headTail - 原子更新: 使用
CompareAndSwap()原子地更新headTail - 无锁保证: 所有操作都是原子操作,无需 mutex
- 环形缓冲区: 通过取模操作实现环形缓冲区
- CAS 重试: 如果 CAS 失败,重新读取并重试,确保正确性
- 延迟分配: block 按需分配,减少内存开销
- 内存安全: pop 后清空引用,避免内存泄漏
mcentral.cacheSpan() 方法
cacheSpan() 是 mcentral 的核心方法,用于从 mcentral 获取 span 给 mcache。该方法完全无锁:
1 | // cacheSpan 从 mcentral 获取一个 span 给 mcache(完全无锁) |
关键点:
- 完全无锁: 所有对
partial和full的操作都是无锁的 - 负载均衡: 根据 P 的 ID 选择不同的 spanSet,减少竞争
- 状态管理: 自动管理 span 在
partial和full之间的转换 - 回退机制: 如果
partial为空,会尝试从full获取(可能有些对象被 GC 释放) - GC 协作: 通过
rebuild()方法回收被 GC 释放的对象 - 延迟分配: 只有在必要时才从 mheap 分配新 span
mcentral 的回收流程
当 span 中的对象被 GC 回收后:
flowchart TD
Start["GC 回收对象"]
CheckSpan["检查 span 状态"]
PartialFree["span 部分空闲?"]
MoveToPartial["移到 partial spanSet"]
CompletelyFree["span 完全空闲?"]
ReturnToMheap["归还给 mheap"]
Start --> CheckSpan
CheckSpan --> PartialFree
PartialFree -->|是| MoveToPartial
PartialFree -->|否| CompletelyFree
CompletelyFree -->|是| ReturnToMheap
CompletelyFree -->|否| Start
MoveToPartial --> Start
partial vs full 的区别
| 特性 | partial | full |
|---|---|---|
| 对象状态 | 有可用对象 | 所有对象都已分配 |
| 分配来源 | 优先从此获取 | 作为备选 |
| 回收后 | 保持在此 | 移到 partial |
| 完全空闲 | 移到 mheap | 移到 mheap |
spanSet 的优势
- 无锁操作: 使用原子操作,减少锁竞争
- 高性能: 环形缓冲区设计,O(1) 插入和删除
- 并发友好: 多个 P 可以并发访问不同的 spanSet
- 内存局部性: span 数组连续存储,提高缓存命中率
新旧版本对比
旧版本(Go 1.17 及之前):
- 使用
nonempty和empty链表 - 需要全局锁(
lock mutex)保护 - 链表操作可能较慢
- 锁竞争严重,影响并发性能
新版本(Go 1.18+):
- 使用
partial和fullspanSet - 完全无锁操作,移除了
lock字段 - 使用原子操作实现并发安全
- 语义更清晰(partial = 部分空闲,full = 完全分配)
- 性能显著提升,特别是在高并发场景下
性能对比:
| 特性 | 旧版本 | 新版本 |
|---|---|---|
| 锁机制 | 全局 mutex | 完全无锁 |
| 并发性能 | 锁竞争严重 | 无锁,性能更好 |
| 数据结构 | 链表 | spanSet(环形缓冲区) |
| 操作复杂度 | O(n) | O(1) |
| 内存开销 | 较低 | 稍高(但值得) |
4. mheap (全局堆)
mheap 是全局的堆管理器,管理所有的内存页和 span。
结构定义(简化)
1 | type mheap struct { |
mheap 的功能
- 内存页管理: 管理所有的内存页(page)
- span 分配: 从空闲页中分配新的 span
- 大对象分配: 直接分配大对象(>32KB)
- 内存回收: 回收空闲的 span 和页
- arena 管理: 通过
arenas数组管理所有的heapArena
arenas 数组结构
mheap.arenas 是一个两级数组,用于快速定位 heapArena:
1 | // arenas 数组结构 |
arenas 数组的查找
flowchart TD
A["内存地址 p"]
B["计算 arena 地址
arena = p &^ (heapArenaBytes - 1)"]
C["计算一级索引 L1
L1 = arena / heapArenaBytes / (1 << arenaL2Bits)"]
D["计算二级索引 L2
L2 = arena / heapArenaBytes % (1 << arenaL2Bits)"]
E["arenas[L1][L2]
获取 heapArena"]
A --> B --> C --> D --> E
style A fill:#ffcccc
style E fill:#ccffcc
arena 结构
graph LR
subgraph arena["arena (堆内存区域)"]
p0["Page 0
(8KB)"]
p1["Page 1
(8KB)"]
p2["Page 2
(8KB)"]
pn["..."]
end
p0 --> span["组成 span"]
p1 --> span
p2 --> span
pn --> span
5. heapArena (堆内存区域)
heapArena 是 Go 运行时中管理实际堆内存区域的核心结构,每个 heapArena 管理一个固定大小的内存区域(通常是 64MB 或 512MB,取决于平台)。
结构定义(简化)
1 | type heapArena struct { |
heapArena 的作用
- 内存区域管理: 管理一个固定大小的连续内存区域(arena)
- 页状态跟踪: 跟踪每个页的分配状态和使用情况
- span 映射: 记录每个页属于哪个 span,支持快速查找
- GC 支持: 提供位图支持垃圾回收的标记和扫描
- 内存分配: 作为实际内存分配的底层存储
arena 大小
不同平台上的 arena 大小不同:
1 | 平台 arena 大小 |
heapArena 与 mheap 的关系
graph TB
subgraph mheap_structure["mheap 结构"]
arenas_array["arenas 数组
[L1][L2]*heapArena"]
end
subgraph arena_structure["heapArena 结构"]
bitmap["bitmap
(页分配位图)"]
spans["spans
(页到span的映射)"]
pageInUse["pageInUse
(页使用位图)"]
pageMarks["pageMarks
(GC标记位图)"]
end
subgraph memory["实际内存区域 (64MB)"]
page0["Page 0 (8KB)"]
page1["Page 1 (8KB)"]
page2["Page 2 (8KB)"]
pagen["..."]
end
arenas_array --> arena_structure
arena_structure --> memory
spans --> page0
spans --> page1
spans --> page2
spans --> pagen
arena 索引计算
mheap 使用两级索引来快速定位 heapArena:
1 | // arena 索引计算 |
heapArena 的位图管理
heapArena 使用位图来高效管理内存:
flowchart TD
A["heapArena (64MB)"]
B["bitmap 位图"]
C["spans 映射数组"]
D["pageInUse 位图"]
E["pageMarks 位图"]
A --> B
A --> C
A --> D
A --> E
B --> B1["标记每个对象的分配状态"]
C --> C1["记录每个页属于哪个span"]
D --> D1["标记页是否在使用"]
E --> E1["GC标记位图"]
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccccff
style D fill:#ffffcc
style E fill:#ffccff
heapArena 的内存布局
graph TB
subgraph arena_memory["heapArena 内存区域 (64MB)"]
subgraph metadata["元数据区域"]
bitmap_region["bitmap
(约2MB)"]
spans_region["spans 数组
(约512KB)"]
pageInUse_region["pageInUse
(约8KB)"]
pageMarks_region["pageMarks
(约8KB)"]
end
subgraph data["数据区域"]
page0_data["Page 0
(8KB)"]
page1_data["Page 1
(8KB)"]
page2_data["Page 2
(8KB)"]
pagen_data["...
(剩余约61MB)"]
end
end
metadata --> data
spans_region --> page0_data
spans_region --> page1_data
spans_region --> page2_data
spans_region --> pagen_data
heapArena 的生命周期
flowchart TD
S1["1. 从操作系统申请内存
(mmap 64MB)"]
S2["2. 创建 heapArena 结构"]
S3["3. 初始化位图和映射"]
S4["4. 注册到 mheap.arenas"]
S5["5. 将页分配给 span"]
S6["6. span 分配给对象"]
S7["7. 对象使用中..."]
S8["8. GC 标记和扫描"]
S9["9. 对象回收,页可能空闲"]
S10["10. 完全空闲的 arena 可能归还系统"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9 --> S10
S9 -.->|可能循环| S5
heapArena 的关键操作
1. 查找页所属的 span
1 | // 根据地址查找对应的 span |
2. 标记页为已使用
1 | // 标记页为已使用 |
3. 检查页是否在使用
1 | // 检查页是否在使用 |
位图操作实现
位图是内存分配器中用于跟踪对象分配状态的核心数据结构:
1 | // gcBits 是位图结构,用于标记对象的分配和 GC 状态 |
位图的关键特性:
- 双位图设计:
allocBits跟踪分配状态,gcmarkBits跟踪 GC 标记 - 高效存储: 每个对象仅占 2 位,内存开销小
- 快速查找: 位操作非常快速,O(1) 复杂度
- GC 协作: 支持高效的 GC 标记和清扫
- 原子操作: 位图操作可以配合原子操作实现并发安全
heapArena 与 GC 的协作
sequenceDiagram
participant GC as 垃圾回收器
participant HA as heapArena
participant Bitmap as bitmap/pageMarks
Note over GC,HA: GC 标记阶段
GC->>HA: 扫描 heapArena
HA->>Bitmap: 读取 pageMarks
Bitmap-->>HA: 返回标记信息
HA->>HA: 标记存活对象
HA-->>GC: 返回标记结果
Note over GC,HA: GC 清扫阶段
GC->>HA: 清扫未标记对象
HA->>HA: 更新 pageInUse
HA->>HA: 更新 spans 映射
HA-->>GC: 清扫完成
heapArena 的优势
- 快速查找: 通过两级索引快速定位 arena
- 位图管理: 使用位图高效管理内存状态
- 内存对齐: arena 大小对齐,便于管理
- GC 友好: 位图支持高效的 GC 标记和扫描
- 并发安全: 通过锁保护,支持并发访问
heapArena 的注意事项
- 内存开销: 每个 arena 需要额外的元数据(位图、映射等)
- 碎片问题: 如果 arena 中只有少量页在使用,可能造成浪费
- 系统限制: 受操作系统虚拟内存限制
- 平台差异: 不同平台的 arena 大小可能不同
内存分配流程
小对象分配(<32KB)
flowchart TD
Start["Goroutine 请求分配内存"]
Step1["1. 检查对象大小"]
Tiny["<16B: 使用 tiny allocator"]
Calc[">=16B: 计算 spanclass"]
Step2["2. 从 mcache 获取 span"]
Check1["mcache.alloc[spanclass]
有可用对象?"]
Alloc1["是: 直接分配,返回"]
Step3["3. 从 mcentral 获取 span"]
Check2["mcentral.partial
有可用 span?"]
Alloc2["是: 获取 span,放入 mcache,分配"]
Step4["4. 从 mheap 分配新 span"]
AllocPage["从空闲页分配"]
CreateSpan["创建新的 span"]
Return["返回给 mcentral,再给 mcache,最后分配"]
Start --> Step1
Step1 --> Tiny
Step1 --> Calc
Calc --> Step2
Step2 --> Check1
Check1 -->|是| Alloc1
Check1 -->|否| Step3
Step3 --> Check2
Check2 -->|是| Alloc2
Check2 -->|否| Step4
Step4 --> AllocPage
AllocPage --> CreateSpan
CreateSpan --> Return
代码示例(简化流程)
1 | // mallocgc 是 Go 内存分配的核心函数 |
关键辅助函数实现
1. size_to_class - 根据对象大小计算 spanclass
1 | // size_to_class 根据对象大小计算对应的 spanclass |
2. tinyAlloc - 微小对象分配器
1 | // tinyAlloc 用于分配微小对象(<16B) |
tiny分配的内存怎么回收
没有针对某一次 tiny 分配的 free。
回收完全由 垃圾回收器(GC) 在做:标记-清扫时,把没人引用的内存还给 span,再被复用。
所以:tiny 申请的内存 = 通过 GC 回收释放。回收的单位:整块 16 字节
Tiny 从 mcache 拿到的是一整块 16 字节(span 里的一个对象),再在这块里用 tinyoffset 切出多个小对象。
Span 只记录“这一块 16 字节是否在用”,不记录这块里每个几字节的小对象。
所以回收时也是以 “这一块 16 字节” 为单位:
- 只要还有任意一个从这块里分出去的指针被引用,整块 16 字节都算存活。
- 当没有任何指向这块里任意位置的指针存活时,整块 16 字节变成垃圾,GC 在 sweep 阶段 会把这 16 字节标记为空闲,归还给 span,供后续 tiny 或同 size class 分配复用。
也就是说:tiny 的“释放” = 某块 16 字节上所有 tiny 分配都不可达后,整块被 GC 回收。
流程简述
- tinyAlloc 分配 → 从 span 拿一块 16B,塞进 c.tiny,在块内按 tinyoffset 切 → 返回指向块内某地址的指针
- 用户不再引用该指针 → 下次 GC:标记阶段发现这块 16B 已不可达 → 清扫阶段:把这块 16B 在 span 里标成 free,可被再次分配
所以:tiny 申请的内存 = 随 GC 自动回收释放,以 16 字节整块为单位,没有单独 free。
3. span.nextFreeFast - 快速分配(使用 freeindex)
1 | // nextFreeFast 快速分配对象(使用 freeindex) |
4. span.nextFree - 使用位图分配
1 | // nextFree 使用位图查找下一个空闲对象 |
5. largeAlloc - 大对象分配
1 | // largeAlloc 分配大对象(>=32KB) |
6. mheap.alloc - 从 mheap 分配 span
1 | // alloc 从 mheap 分配指定页数的 span |
大对象分配(>=32KB)
flowchart TD
Start["Goroutine 请求分配大对象"]
Step1["1. 计算需要的页数"]
Step2["2. 直接从 mheap 分配"]
FindPage["查找空闲页"]
AllocPage["分配连续页"]
CreateSpan["创建 span"]
Step3["3. 返回对象地址"]
Start --> Step1
Step1 --> Step2
Step2 --> FindPage
FindPage --> AllocPage
AllocPage --> CreateSpan
CreateSpan --> Step3
大对象分配特点
- 直接分配: 不经过 mcache 和 mcentral
- 页对齐: 按页(8KB)对齐分配
- 独立管理: 大对象 span 单独管理
- GC 友好: 大对象更容易被 GC 回收
组件之间的交互关系
1. mcache ↔ mcentral
sequenceDiagram
participant M as mcache (P 本地)
participant C as mcentral
participant SS as spanSet (无锁)
participant H as mheap
Note over M,C: 当 mcache 的 span 用完时:
M->>C: 1. 请求新的 span (cacheSpan)
C->>SS: 2. 从 partial spanSet 取出 (无锁 pop)
alt partial 为空
C->>SS: 3. 尝试从 full spanSet 获取 (无锁 pop)
alt full 也为空
C->>H: 4. 从 mheap 获取 (需要锁)
H-->>C: 返回新 span
else full 有 span
SS-->>C: 返回 span
end
else partial 有 span
SS-->>C: 返回 span
end
C->>C: 5. 如果 span 分配完,移到 full (无锁 push)
C->>M: 6. 返回 span 给 mcache
Note over M,C: 所有 mcentral 操作都是无锁的!
2. mcentral ↔ mheap
sequenceDiagram
participant C as mcentral
participant H as mheap
Note over C,H: 当 mcentral 的 span 用完时:
C->>H: 1. 请求新的 span
H->>H: 2. 从空闲页分配
H->>H: 3. 创建新的 span
H->>C: 4. 返回给 mcentral
3. mheap ↔ heapArena
sequenceDiagram
participant H as mheap
participant A as heapArena
participant OS as 操作系统
Note over H,OS: 分配新 arena
H->>OS: 1. 申请内存(mmap 64MB)
OS-->>H: 返回内存地址
H->>H: 2. 创建 heapArena 结构
H->>A: 3. 初始化位图和映射
H->>H: 4. 注册到 arenas[L1][L2]
Note over H,A: 查找 arena
H->>H: 根据地址计算 L1, L2
H->>A: 从 arenas[L1][L2] 获取
Note over H,A: 管理页和 span
H->>A: 查询页所属的 span
A-->>H: 返回 span 指针
H->>A: 更新页状态位图
A-->>H: 确认更新
mheap 管理 heapArena 的流程
flowchart TD
H["mheap"]
Arenas["arenas 数组
[L1][L2]*heapArena"]
HA["heapArena"]
Memory["实际内存 (64MB)"]
H --> Arenas
Arenas --> HA
HA --> Memory
subgraph Process["heapArena 管理流程"]
P1["1. 从操作系统申请内存(mmap)"]
P2["2. 创建 heapArena 结构"]
P3["3. 初始化位图和 spans 映射"]
P4["4. 注册到 mheap.arenas"]
P5["5. 将页分配给 span"]
P6["6. 管理页的分配和回收"]
P1 --> P2 --> P3 --> P4 --> P5 --> P6
end
完整交互流程图
flowchart TD
G["Goroutine"]
M["mcache
(P 本地)"]
C["mcentral
(中心缓存)"]
H["mheap
(全局堆)"]
Arenas["arenas 数组"]
HA["heapArena
(64MB)"]
Memory["实际内存
(页和对象)"]
G -->|分配请求| M
M -->|span 用完
需要新 span| C
M -.->|返回| M
C -->|需要新 span| H
H -->|查找/分配| Arenas
Arenas -->|定位| HA
HA -->|管理| Memory
H -->|直接管理| Memory
flowchart TB subgraph pages [pages] pages1 pages2 pages3 pages4 end subgraph heap_arena [heap_arena] heap_arena1 heap_arena2 heap_arena3 heap_arena4 end subgraph mheap [mheap] mheap1[mheap] end subgraph mcentral [mcentral] mcentral1 mcentral2 mcentral3 mcentral4[...] end subgraph mcache [mcache] mcache1 mcache2 mcache3 mcache4 end heap_arena1 --> pages2 heap_arena4 --> pages1 heap_arena2 --> pages3 heap_arena3 --> pages4 mheap1 --> heap_arena1 mheap1 --> heap_arena4 mheap1 --> heap_arena2 mheap1 --> heap_arena3 mcentral1 --> mheap1 mcentral2 --> mheap1 mcentral3 --> mheap1 mcentral4 --> mheap1 mcache1 --> mcentral1 mcache2 --> mcentral2 mcache3 --> mcentral3 mcache4 --> mcentral4
内存分配优化策略
1. 分级分配
根据对象大小使用不同的分配策略:
flowchart TD
Start["对象大小"]
Tiny["微小对象 (<16B)"]
Small["小对象 (16B-32KB)"]
Large["大对象 (>=32KB)"]
Tiny1["使用 tiny allocator"]
Tiny2["多个小对象打包在一个内存块中"]
Small1["使用 mcache → mcentral → mheap"]
Small2["按 spanclass 分类管理"]
Large1["直接从 mheap 分配"]
Large2["按页对齐"]
Start --> Tiny
Start --> Small
Start --> Large
Tiny --> Tiny1
Tiny1 --> Tiny2
Small --> Small1
Small1 --> Small2
Large --> Large1
Large1 --> Large2
2. 多级缓存
减少锁竞争,提高并发性能:
flowchart TD
L1["Level 1: mcache
(无锁,最快)"]
L2["Level 2: mcentral
(需要锁,但缓存复用)"]
L3["Level 3: mheap
(需要锁,从系统分配)"]
L1 -->|span 用完| L2
L2 -->|span 用完| L3
3. 内存池化
复用已分配的内存,减少系统调用:
flowchart LR
A["已分配的 span"]
B["使用完"]
C["归还 mcentral"]
D["复用"]
A --> B --> C --> D
D -.->|循环| A
4. 按需分配
延迟分配,减少内存占用:
flowchart LR
A["不预先分配所有内存"]
B["只在需要时从系统申请"]
A --> B
内存回收机制
GC 与内存分配器的协作
flowchart TD
subgraph Mark["GC 标记阶段"]
M1["扫描 mcache 中的 span"]
M2["扫描 mcentral 中的 span"]
M3["扫描 mheap 中的 span"]
M4["标记存活对象"]
M1 --> M2 --> M3 --> M4
end
subgraph Sweep["GC 清扫阶段"]
S1["清扫未标记的对象"]
S2["将完全空闲的 span 归还 mheap"]
S3["将部分空闲的 span 放回 mcentral"]
S1 --> S2 --> S3
end
Mark --> Sweep
span 的生命周期
flowchart TD
S1["1. 从 mheap 分配"]
S2["2. 放入 mcentral"]
S3["3. 分配给 mcache"]
S4["4. 分配对象给 goroutine"]
S5["5. 对象使用中..."]
S6["6. GC 标记"]
S7["7. 对象被回收"]
S8["8. span 部分空闲 → 放回 mcentral"]
S9["9. span 完全空闲 → 归还 mheap"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7 --> S8 --> S9
S8 -.->|可能循环| S3
S9 -.->|可能循环| S1
实际应用和优化
1. 减少内存分配
对象池(sync.Pool)
1 | package main |
预分配切片容量
1 | // 不好:频繁扩容 |
2. 理解内存分配行为
查看内存分配
1 | package main |
使用 pprof 分析
1 | # 获取内存 profile |
3. 优化建议
避免频繁的小对象分配
1 | // 不好:每次分配新的字符串 |
使用值类型而非指针
1 | // 小结构体使用值类型 |
减少指针的使用
1 | // 不好:大量指针 |
内存分配器调优
1. GOGC 参数
控制 GC 的频率:
1 | # 默认值:100 |
2. 内存限制
1 | # 设置最大内存使用 |
3. 调试内存分配
1 | package main |
常见问题
1. 内存泄漏
检测方法
1 | // 定期检查内存增长 |
使用 pprof
1 | # 对比两个时间点的 heap profile |
2. 内存碎片
内存碎片会导致:
- 无法分配大块连续内存
- 内存使用率低
- GC 压力增大
解决方案:
- 使用对象池减少分配
- 预分配大块内存
- 定期整理内存
3. GC 压力大
表现:
- GC 时间占比高
- 程序响应慢
- CPU 使用率高
解决方案:
- 减少内存分配
- 使用对象池
- 调整 GOGC 参数
- 优化数据结构
总结
Go 内存分配器的核心特点:
- 分级分配: 根据对象大小使用不同策略
- 多级缓存: mcache → mcentral → mheap
- 完全无锁的 mcentral: 使用
spanSet实现完全无锁操作,显著提升并发性能 - 并发优化: 每个 P 有独立的 mcache,mcentral 也完全无锁,减少锁竞争
- 内存复用: span 可以复用,减少系统调用
- GC 友好: 与 GC 紧密协作,高效回收内存
核心组件关系:
flowchart TD
G["Goroutine"]
M["mcache
(P 本地,无锁)"]
C["mcentral
(中心缓存,有锁)"]
H["mheap
(全局堆,有锁)"]
Arenas["arenas 数组
(两级索引)"]
HA["heapArena
(64MB,管理实际内存)"]
G --> M
M -->|span 用完| C
C -->|span 用完| H
H --> Arenas
Arenas --> HA
heapArena 的关键作用:
- 内存区域管理: 每个
heapArena管理 64MB 的连续内存区域 - 快速定位: 通过两级索引
arenas[L1][L2]快速定位 - 位图管理: 使用位图高效跟踪页和对象的分配状态
- span 映射: 通过
spans数组快速查找页所属的 span - GC 支持: 提供位图支持垃圾回收的标记和扫描
优化建议:
- 减少内存分配(使用对象池、预分配)
- 减少指针使用(减少 GC 压力)
- 理解分配行为(使用 pprof 分析)
- 合理设置 GC 参数(GOGC、GOMEMLIMIT)
- 利用无锁设计: Go 1.18+ 的 mcentral 完全无锁,高并发场景下性能优异
最新改进(Go 1.18+):
- mcentral 完全无锁: 移除了
lock字段,使用spanSet实现无锁操作 - 性能提升: 无锁设计显著提升了高并发场景下的内存分配性能
- 更好的并发: 多个 P 可以并发访问 mcentral,无锁竞争
理解 Go 内存分配器的工作原理,有助于编写高性能的 Go 程序,减少内存分配和 GC 压力。