Span和mspan的关系在go中是怎么定义的
在 Go 语言底层实现中,“Span” 并不是一个独立的关键字或类型,而是指由
一个
我们可以认为 mspan是span的物理实现。mspan 结构体定义了如何管理某段Span内存,所以我们可以这样认为 Span代表一段连续的物理内存,是抽象的概念。
mspan 结构体管理的一段连续的、以 Page(页)为单位的内存空间。一个
mspan 管理着一个 Span(一段连续内存),且一个 mspan 只能有一种规格。在 Go 的 TCMalloc 模型中,规格(Size Class)是属于 mspan 结构体的属性我们可以认为 mspan是span的物理实现。mspan 结构体定义了如何管理某段Span内存,所以我们可以这样认为 Span代表一段连续的物理内存,是抽象的概念。
而mspan才是具体的实现span。
规则:一个
物理表现:一旦规格确定,这个
结论:不存在“一个 Span 包含多种规格”的情况。如果一段内存里既有 8 字节对象又有 16 字节对象,那么它们一定分属于两个不同的
规则:一个
mspan 对象在初始化时,必须指定一个唯一的 spanClass,一个 mspan 对象代表某个规格的Span内存。一个mspan对象在它的生命周期内,只能代表一种规格。
mspan 代表某种规格时,它实际上固定了以下参数:
mspan.elemsize(元素大小):每个对象的长度(如 16B)。
mspan.nelems(元素数量):这段内存总共能放多少个对象。
mspan.noscan(扫描属性):这种规格的对象是否包含指针(如果不含指针,GC 时将直接跳过,显著提升性能)。
物理表现:一旦规格确定,这个
mspan 管理的物理内存(Span)就会被平均切分成 N 个大小完全一致的坑位结论:不存在“一个 Span 包含多种规格”的情况。如果一段内存里既有 8 字节对象又有 16 字节对象,那么它们一定分属于两个不同的
mspan如果你在源码中寻找它的定义,它核心体现在
runtime 包中的 mspan 结构体及相关的常量。1. 物理层面的定义:连续的 Page
从物理意义上讲,一个 Span 是由一个或多个连续的 Page 组成的内存块。
- 最小单位:1 个 Page(在 Go 中固定为 8KB,即
_PageSize)。
_PageSize = 1 << _PageShift //1<<13 = 8192 , _PageShift=13 定义在 C:\Go\src\runtime\malloc.go
- 跨度:由常量
class_to_allocnpages决定。例如,某些规格的 Span 跨度为 1 页(8KB),某些大规格的可能跨度为 4 页(32KB)或更多。
2. 逻辑层面的定义:
mspan 结构体 在
src/runtime/mheap.go 中,mspan 结构体定义了如何管理这段内存(span):type mspan struct { // 1. 物理地址信息 next *mspan // 双向链表下一项 prev *mspan // 双向链表前一项 startAddr uintptr // 该 Span 在虚拟内存中的起始地址(定义了这段内存从虚拟地址的哪个点开始) npages uintptr // 该 Span 包含的页数(每页 8KB),定义了这段地盘有多大(由多少个 8KB 的 Page 组成)
// 2. 规格信息 spanclass spanClass // 规格等级(Size Class)和标记(是否包含指针) elemsize uintptr // 单个对象的大小(如 8byte, 16byte, 32byte等),注意一个page=8k是远大于对象大小的 nelems uintptr // 该 Span 中总共可以存放的对象个数,一个page会包括多个对象, 1 个 Page 的容量远大于 1 个 32 字节的对象。 // 对象数量= PageSize/elemsize
// 假如对象大小为32k, 例如: 8192/32=256 ,这1个连续的Page被逻辑切分成了256个连续的32字节对象坑位。
// 3. 分配状态管理 freeindex uintptr // 下一个空闲对象的索引 allocBits *gcBits // 标记哪些对象已被分配的位图 gcmarkBits *gcBits // GC 标记阶段使用的位图 // ... 其他元数据 }
我们看下 mspan 和spanclass elemsize nelems 用一个格子模型展示一下:
mspan 管理结构体 +-------------------------------------------------------------+ | [spanclass: 3] [elemsize: 32bye] [nelems: 256] [npages: 1] | | [freeindex: 2] [allocBits: 1100...] [gcmarkBits: 1000...] | +-------------------------------------------------------------+ | | 管理/指向 v 物理内存跨度Span - 总长度是 8192 字节 (8KB) +-------------------------------------------------------------+ | Index 0 | Index 1 | Index 2 | Index 3 | ... Index 255 | | (32 Bytes)| (32 Bytes)| (32 Bytes)| (32 Bytes)| ... (32 Bytes)| +-----------+-----------+-----------+-----------+---------------+ | [Object] | [Object] | [ 空闲 ] | [ 空闲 ] | ... [ 空闲 ] | +-----------+-----------+-----------+-----------+---------------+ |<- elemsize ->|
3. Span 的生命周期定义
在 Go 运行时的内存层级中,Span 的地位如下:
- 从 Arena 分配:当需要新的 Span 时,
mheap从heapArena中划出 N 个连续的 Page。 - 切分与规格化:根据分配请求的 Size Class,这段内存被“格式化”。例如,一个 8KB 的 Span 被切分为 1024 个 8 字节的对象。
- 放入缓存:
mspan被放入 P(处理器)的mcache中,供程序极速分配。
4. 关键特性
- 单一规格性:一个
mspan内的所有对象大小必须完全一致。如果你申请 16 字节,你只会从负责 16 字节规格的mspan中获得内存。 - 页对齐:所有的 Span 起始地址必须是 Page(8KB)的整数倍。
- 元数据分离:Span 的实际数据在 Arena 中,而管理它的
mspan结构体(元数据)则存储在专门的mheap管理区,这种分离保证了用户数据的纯净和缓存友好性。
总结
在 Go 中,Span 是内存分配的“施工段”。它代表了:“从地址 A 开始,后面连续的 N 个 Page 内存,现在全部按 X 字节一个坑位进行切分,由这个
mspan 对象负责管账。”

浙公网安备 33010602011771号