Span和mspan的关系在go中是怎么定义的

在 Go 语言底层实现中,“Span” 并不是一个独立的关键字或类型,而是指由 mspan 结构体管理的一段连续的、以 Page(页)为单位的内存空间

一个 mspan 管理着一个 Span(一段连续内存),且一个 mspan 只能有一种规格。在 Go 的 TCMalloc 模型中,规格(Size Class)是属于 mspan 结构体的属性

我们可以认为 mspan是span的物理实现。mspan 结构体定义了如何管理某段Span内存,所以我们可以这样认为 Span代表一段连续的物理内存,是抽象的概念。
而mspan才是具体的实现span。

规则:一个 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 的地位如下:
  1. 从 Arena 分配:当需要新的 Span 时,mheap 从 heapArena 中划出 N 个连续的 Page。
  2. 切分与规格化:根据分配请求的 Size Class,这段内存被“格式化”。例如,一个 8KB 的 Span 被切分为 1024 个 8 字节的对象。
  3. 放入缓存:mspan 被放入 P(处理器)的 mcache 中,供程序极速分配。
 
4. 关键特性
  • 单一规格性:一个 mspan 内的所有对象大小必须完全一致。如果你申请 16 字节,你只会从负责 16 字节规格的 mspan 中获得内存。
  • 页对齐:所有的 Span 起始地址必须是 Page(8KB)的整数倍。
  • 元数据分离:Span 的实际数据在 Arena 中,而管理它的 mspan 结构体(元数据)则存储在专门的 mheap 管理区,这种分离保证了用户数据的纯净和缓存友好性。
 
总结
在 Go 中,Span 是内存分配的“施工段”。它代表了:“从地址 A 开始,后面连续的 N 个 Page 内存,现在全部按 X 字节一个坑位进行切分,由这个 mspan 对象负责管账。”
posted @ 2026-01-26 23:05  jinzi  阅读(0)  评论(0)    收藏  举报