golang系列之-内存分配
golang的内存分配机制最初是基于TCMalloc,演化至今已经有了很大差异。其原理是:slab + tiling algorithm + 层级内存分配。本文仅介绍如何通过mallocgc分配内存,不涉及栈内存分配管理、手动内存管理等内容。当前go版本:1.24
前言
slab + tiling
内存分配基本单位-mspan(即slab),内部再划分更小的块(即tiling)
小对象(<=32KB)按预定义的sizeclass分成不同的mspan,每个mspan最低有8KB,切割成指定大小的块。而大对象(>32KB)的sizeclass=0,不限制大小,mspan不分块。如下
1 | // mspann -> sizeclass=0 -> nKB -> | <----- nKB -----> | |
如make([]int, 5)
,创建一块40B大小的内存区域,经过计算这个内存块会从sizeclass=5的mspan分配(该mspan的每个元素大小为48B)
层级内存分配
参考TCMalloc,内存分配时主要分为3级:mcache、mcentral、mheap
- mcache为线程缓存,每个p都有一个,所有的内存分配/回收都是先通过mcache,访问不需要加锁
- mcentral负责向mheap申请分配内存、管理mspan,按spanclass分组,嵌入到mheap结构体,全局唯一
- mheap负责从系统申请内存进行分配管理,一般情况下访问都要加锁,全局唯一
执行mallocgc分配内存时,mheap、mcentral、mcache的关系如下
1 | // mallocgc |
- 如果对象<=32KB
- 从p的mcache找一个空闲的mspan分配一段内存/地址空间
- 如果mspan没有空闲空间,从mcentral申请一个新的/重用一个已清扫的mspan
- 如果mcentral也没有可用mspan,通过mheap跟OS申请一段内存,初始化一个mspan返回
- 如果对象>32KB
- 通过mheap跟OS申请一段内存,初始化一个mspan返回
系统内存状态
这里需要了解系统内存的几个状态,以及go内部是如何从系统分配、释放内存的
状态 | 含义 |
---|---|
None | 默认状态,未映射,地址空间未被保留或使用 |
Reserved | 已保留,但未提交,即地址空间已经被申请,但尚未向操作系统请求实际物理内存 |
Prepared | 已提交但未使用,已经向操作系统申请了物理内存,但可能未完全初始化 |
Ready | 可用状态,内存已初始化,可用于分配 |
状态转换函数
函数 | 主要作用 | 是否分配物理内存 | 是否映射虚拟内存 | 内存状态转换 | 备注 |
---|---|---|---|---|---|
sysAlloc |
直接申请内存 | ✅ 是 | ✅ 是 | None -> Ready | 可能会触发 mmap |
sysReserve |
保留虚拟地址空间,但不映射 | ❌ 否 | ✅ 是 | None -> Reserved | 预留地址,后续 sysMap |
sysMap |
将预留的虚拟地址映射为实际物理内存 | ✅ 是 | ✅ 是 | Reserved -> Prepared | 只有 sysReserve 过的地址能 sysMap |
sysUsed |
标记某段地址正在使用 | ✅ 是 | ✅ 是 | Prepared -> Ready | 可能会触发 madvise 让物理页生效 |
sysUnused |
标记某段地址未使用,可以回收物理内存 | ⚠️ 可能 | ❌ 否 | Ready -> Prepared | MADV_DONTNEED ,内存仍属于进程 |
sysFault |
让一段地址变成不可访问 | ❌ 否 | ✅ 是 | Ready -> Reserved | mprotect(PROT_NONE) ,用于调试 |
sysFree |
释放虚拟内存,归还给 OS | ✅ 是 | ✅ 是 | -> None | munmap ,这段内存不能再用 |
我对Go内存分配的理解
- 内存管理的本质是地址空间的组织和维护
- Go的内存策略:尽量保留虚拟地址,按需释放物理页
- Go会一次性申请64MB内存(Reserved-虚拟地址空间),但并不是立即分配物理页,而是在需要分配时使用sysMap + sysUsed使一小部份内存可用(Reserved->Prepared->Ready)
GC助攻
为了防止内存分配速度过快,导致GC跟不上,分配时会判断是否需要协助GC标记/清扫。每次都要判断是否需要协助标记,而清扫只发生在获取新的mspan和大对象分配场景下