[!NOTE]

💡 本文基于 Go 1.25.3 源码编写,相比 Go 1.16 版本,增加了 User Arena、Weak Pointer、Cleanup 机制等重要特性。后续版本可能会有变化。建议结合实际使用的 Go 版本阅读相关源码。

结论先行

Go内存模型架构

Go 的内存分配器设计源于 TCMalloc,采用了多层级缓存架构来减少锁竞争并提高性能。核心设计如下:

核心架构

  • Go 将堆内存抽象为 mheap 结构体;
  • Go 进程会从虚拟内存中申请 n 个 heapArena(64位系统每个 64MB);
  • 每个 heapArena 被按需划分成不同 class 的 mspan,共有 68 个 size class;
  • 每个 mspan 由 n 个相同大小的 span 组成;
  • 为了快速定位合适的 span,为 mheap 建立了 136 个中央索引 mcentral
  • 每个 mcentral 存储对应 class 的 mspan,每种 mspan 又划分为 gc scan 和 no scan 两种,故共有 68 × 2 = 136 个 mcentral;
  • 为了解决中央索引的并发锁竞争问题,为每一个 P(线程)建立一个本地缓存 mcache
  • 每个 mcache 存储 136 个 span,分别是每种 class 的 mspan 的一个 scan 和 noscan 的 span。

内存分配策略

  • Go 中根据对象大小分为 tinysmalllarge 三种对象;
  • tiny (0~16B 无指针) 对象主要分配到 class 2 的 span 中(通过 tiny allocator);
  • small (16B~32KB) 对象会被分配到 class 2 ~ class 67 的 span 中;
  • class 1 (8B) 仅用于 64 位平台上的单指针对象,使用极少;
  • large (>32KB) 对象会量身定做分配到 class0 的 span 中,直接从 mheap 上申请;
  • 为对象分配内存时,会先从 mcache 上找 span,找不到就去 mcentral 上交换,还找不到就去 mheap 上申请,最后找不到就 OOM。

一、协程栈

1.1 作用

协程栈是 Go 协程执行的核心数据结构,主要用于:

  • 记录执行路径:追踪函数调用链
  • 存储局部变量:每个栈帧保存函数的局部变量
  • 函数传参:通过栈传递函数参数
  • 保存返回值:存储函数的返回值

1.2 位置

  • Go 协程栈位于 Go 堆内存上(而非操作系统栈)
  • Go 堆内存位于操作系统虚拟内存
  • 这种设计使得 Go 可以灵活管理协程栈的大小

1.3 图解

以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func sum(a, b int) int {
sum := 0
sum = a + b
return sum
}

func main() {
a := 3
b := 5
print(sum(a, b))
}

栈帧结构如下:

协程栈结构

1.4 参数传递

Go 采用值传递

  • 传递结构体时:拷贝结构体中的全部内容
  • 传递结构体指针时:拷贝结构体指针(8 字节)

1.5 栈大小

Go 1.25.3 中,协程栈的初始大小为 2KB,相比早期版本(如 Go 1.2 的 8KB)更加轻量。

1.6 逃逸分析

不是所有的变量都能放在协程栈上。以下三种情况会导致变量逃逸到堆上:

1. 指针逃逸

函数返回局部变量的指针:

1
2
3
4
func newInt() *int {
x := 42
return &x // x 逃逸到堆
}

2. 空接口逃逸

函数参数为 interface{},编译器无法确定具体类型:

1
2
3
func println(v interface{}) {  // v 可能逃逸
// ...
}

3. 大变量逃逸

变量太大,栈帧放不下。在 64 位机器中,一般超过 64KB 的变量就会逃逸。

1.7 栈扩容

Go 栈的初始空间为 2KB。在函数调用前会执行 morestack 判断栈空间是否足够。

栈扩容策略演进

  • 分段栈(Go 1.3 之前)
    • 优点:没有空间浪费
    • 缺点:栈帧在不连续的空间之间横跳,性能较差("热分裂"问题)
  • 连续栈(Go 1.3 及之后)
    • 优点:空间连续,性能更好
    • 缺点:扩容时需要拷贝,开销较大
    • 策略:小于 1KB 时翻倍,否则增长 25%

二、虚拟内存单元 heapArena

2.1 概述

  • 在物理内存为 64GB 的机器中,每个 Go 进程最多可被分配到 256TB 的虚拟内存
  • Go 的虚拟内存单元为 heapArena,每次申请 64MB(64 位非 Windows 系统)
  • 最多可以申请 2²⁰ (约 100 万) 个 heapArena
  • 所有的 heapArena 组成了 mheap(Go 堆内存)
heapArena 结构

💡 相关阅读操作系统 - 虚拟内存

2.2 底层结构

heapArena 定义在 runtime/mheap.go#L266

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type heapArena struct {
_ sys.NotInHeap

// spans 将 arena 中的虚拟页 ID 映射到 mspan
spans [pagesPerArena]*mspan

// pageInUse 是位图,标记哪些页正在被使用
pageInUse [pagesPerArena / 8]uint8

// pageMarks 用于 GC,标记哪些 span 有被标记的对象
pageMarks [pagesPerArena / 8]uint8

// pageSpecials 标记哪些 span 有 special 记录(finalizer 等)
pageSpecials [pagesPerArena / 8]uint8

// pageUseSpanInlineMarkBits 标记使用内联 mark bits 的 span
pageUseSpanInlineMarkBits [pagesPerArena / 8]uint8

// checkmarks 用于 GC 调试
checkmarks *checkmarksMap

// zeroedBase 标记第一个未使用且已归零的页
zeroedBase uintptr
}

2.3 分配策略对比

线性分配 链表分配 分级分配
线性分配 链表分配 分级分配
实现简单,但内存碎片较多 将空闲块连接起来,牺牲部分性能来缓解内存碎片 将内存按级别分成很多块,根据对象大小存放在能容纳它的最小块中

Go 采用分级分配策略,参考了 TCMalloc,将每一个级定义为 mspan

三、内存管理单元 mspan

3.1 概述

  • Go 使用内存时的基本单位是 mspan
  • 每个 mspan 由 N 个相同大小的 span 组成
  • Go 1.25.3 中有 68 种 size class(class 0 ~ class 67)

Size Class 表(部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
// runtime/sizeclasses.go

// class bytes/obj bytes/span objects tail waste max waste
// 0 0 0 0 0 0.00%
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 24 8192 341 8 29.24%
// 4 32 8192 256 0 11.72%
// ...
// 64 24576 24576 1 0 11.45%
// 65 27264 81920 3 128 10.00%
// 66 28672 57344 2 0 4.91%
// 67 32768 32768 1 0 12.50%
mspan 结构

3.2 底层结构

mspan 定义在 runtime/mheap.go#L420

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type mspan struct {
_ sys.NotInHeap

// 双向链表
next *mspan
prev *mspan
list *mSpanList

// 内存地址和大小
startAddr uintptr // 起始地址
npages uintptr // 页数(每页 8KB)

// 分配信息
freeindex uint16 // 下一个空闲对象的索引
freeIndexForScan uint16 // GC 扫描器使用的索引(Go 1.19+)
nelems uint16 // 对象总数
allocCount uint16 // 已分配对象数

// 位图
allocBits *gcBits // 分配位图
gcmarkBits *gcBits // GC 标记位图
pinnerBits *gcBits // 固定对象位图(Go 1.21+)

// 元数据
spanclass spanClass // size class 和 noscan 标志
elemsize uintptr // 对象大小
state mSpanStateBox // mspan 状态
sweepgen uint32 // 清扫代数

// 新增字段(Go 1.20+)
isUserArenaChunk bool // 是否为 user arena chunk
userArenaChunkFree addrRange // user arena 管理

// GreenTeaGC 实验字段(Go 1.24+)
scanIdx uint16 // 扫描索引

// 大对象类型信息(Go 1.22+)
largeType *_type

// Special 记录
speciallock mutex
specials *special
}

3.3 关键字段说明

1. 分配位图(allocBits)

使用位图标记对象是否已分配: - 0 表示空闲 - 1 表示已分配

2. 双索引设计(Go 1.19+)

  • freeindex:分配器使用
  • freeIndexForScan:GC 扫描器使用

这样设计避免了竞争条件,确保 GC 只在对象完全初始化后才能看到它。

3. 状态机

1
2
3
4
5
const (
mSpanDead mSpanState = iota // 未使用
mSpanInUse // 正在使用
mSpanManual // 手动管理(如栈)
)

四、中心索引 mcentral

4.1 概述

heapArena 中的 mspan 不是一开始就全部划分好的,而是按需划分

由于每个 heapArena 中的 mspan 分布是动态的,为了给要分配空间的对象快速定位到合适的 mspan,Go 定义了中心索引 mcentral

  • 总共有 136mcentral 结构体
  • 其中 68 个用于需要 GC 扫描的对象(scan)
  • 另外 68 个用于无需 GC 扫描的对象(noscan)
mcentral 结构

4.2 底层结构

mcentral 定义在 runtime/mcentral.go#L22

1
2
3
4
5
6
7
8
9
type mcentral struct {
_ sys.NotInHeap

spanclass spanClass // size class 级别

// 双缓冲设计:配合 GC 的 sweepgen
partial [2]spanSet // 有空闲对象的 span 列表
full [2]spanSet // 无空闲对象的 span 列表
}

4.3 双缓冲机制

mcentral 使用双缓冲配合 GC 的清扫机制:

  • sweepgen 每次 GC 增加 2
  • partial[sweepgen/2%2] 是已清扫的 span
  • partial[1-sweepgen/2%2] 是未清扫的 span

这种设计使得 GC 和分配可以并发进行,无需等待所有 span 都清扫完毕。

五、线程缓存 mcache

5.1 概述

mcentral 是一个中心索引,修改它需要使用互斥锁进行保护,锁竞争会造成性能问题。

Go 参考 GMP 模型,为每个 P(逻辑处理器)建立了线程本地缓存 mcache,极大缓解了并发锁争夺的性能消耗。

设计要点: - 每个 P 有一个 mcache - 对于每一种 size class,取一个 scan 和一个 noscan span - 一个 mcache 拥有 136mspan(68 个 scan + 68 个 noscan) - 当本地缓存用完后,才需要上锁去 mcentral 交换

mcache 结构

5.2 底层结构

mcache 定义在 runtime/mcache.go#L20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type mcache struct {
_ sys.NotInHeap

// 内存分析相关
nextSample int64 // 触发堆采样的字节数
memProfRate int // 缓存的内存分析速率
scanAlloc uintptr // 可扫描对象的已分配字节数

// 微对象分配器(<16B 的对象)
tiny uintptr // 当前 tiny block 的起始地址
tinyoffset uintptr // tiny block 中的偏移
tinyAllocs uintptr // tiny 分配次数

// 核心:136 个 mspan
alloc [numSpanClasses]*mspan // numSpanClasses = 136

// 栈缓存
stackcache [_NumStackOrders]stackfreelist

// GC 相关
flushGen atomic.Uint32 // 上次 flush 时的 sweepgen
}

5.3 与 P 的关系

1
2
3
4
5
type p struct {
// ...
mcache *mcache
// ...
}

每个 P 持有一个 mcache 指针,实现无锁快速路径

六、堆 mheap

mheap 是 Go 堆内存的全局管理者,统筹所有内存分配。

6.1 底层结构

mheap 定义在 runtime/mheap.go#L64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
type mheap struct {
_ sys.NotInHeap

// 全局锁(必须在系统栈上获取)
lock mutex

// 页分配器
pages pageAlloc

// GC 相关
sweepgen uint32 // 清扫代数
pagesInUse atomic.Uintptr // 使用中的页数
pagesSwept atomic.Uint64 // 已清扫的页数
sweepPagesPerByte float64 // 比例清扫速率

// Arena 管理(二级映射)
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
heapArenas []arenaIdx // 所有已分配的 arena
curArena struct { // 当前正在增长的 arena
base, end uintptr
}

// 中心索引(136 个)
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}

// mspan 是按需分级的,这里保存所有已划分的 mspan
allspans []*mspan

// 各种 fixalloc 分配器
spanalloc fixalloc // 分配 mspan
cachealloc fixalloc // 分配 mcache
specialfinalizeralloc fixalloc // 分配 finalizer
specialWeakHandleAlloc fixalloc // 分配弱指针(Go 1.23+)
specialCleanupAlloc fixalloc // 分配 cleanup(Go 1.24+)
specialPinCounterAlloc fixalloc // 分配 pin counter(Go 1.21+)
// ... 更多 special 分配器

speciallock mutex // 保护 special 分配器
arenaHintAlloc fixalloc

// 【新特性】User Arena 状态(Go 1.20+)
userArena struct {
arenaHints *arenaHint
quarantineList mSpanList // 等待释放的 span
readyList mSpanList // 可复用的 span
}
userArenaArenas []arenaIdx

// 【新特性】Cleanup ID 计数器(Go 1.24+)
cleanupID uint64

// 【新特性】弱指针映射(Go 1.23+)
immortalWeakHandles immortalWeakHandleMap
}

6.2 关键设计

1. 二级映射(arenas)

为了支持稀疏的虚拟地址空间,使用二级数组: - L1 map:索引 arena 组 - L2 map:索引具体的 heapArena

在大多数 64 位平台上,arenaL1Bits = 0,退化为单级映射。

2. Cache Line 对齐

1
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte

填充字节避免伪共享(false sharing),提升多核性能。

3. 页回收器

1
2
reclaimIndex  atomic.Uint64  // 下一个要回收的页索引
reclaimCredit atomic.Uintptr // 额外回收的页的信用

后台异步回收未使用的页,减少内存占用。

七、内存分配

7.1 对象分级

Go 根据对象大小将分配分为三类:

类型 大小范围 分配方式 Size Class
Tiny 0 ~ 16B(无指针) 多个对象合并到 16B class 2
Tiny 8B(单指针) 64 位上使用 class 1 class1
Small 16B ~ 32KB 从 mcache 分配 class 2 ~ 67
Large > 32KB 直接从 mheap 分配 class 0

注意:Class 1 (8B) 在实践中使用极少,仅在 64 位平台上分配恰好 8 字节且包含指针的对象时使用。绝大多数 8 字节对象要么无指针(走 tiny allocator),要么是结构体的一部分。

7.1.1 Tiny 对象分配

对于 < 16B无指针的对象,Go 使用特殊的 tiny allocator

1
2
3
4
// mcache 中的 tiny allocator
tiny uintptr // 当前 tiny block 起始地址
tinyoffset uintptr // 已使用的偏移量
tinyAllocs uintptr // tiny 分配计数
  1. 尝试在当前 tiny block 中分配(根据对齐要求)
  2. 如果空间不足,从 class 2 (16B) 的 span 中获取新的 tiny block
  3. 多个 tiny 对象共享同一个 16B 块,减少内存浪费
Tiny 对象分配

Class 1 的特殊性:Class 1 (8B) 在实践中使用极少,仅在 64 位平台上分配恰好 8 字节且包含指针的对象时使用。典型例子如单个逃逸的指针变量。由于这种场景非常罕见,class 1 基本处于"保留但不常用"的状态。大多数 8 字节对象要么:

  • 无指针 → 走 tiny allocator(class 2)
  • 是结构体字段的一部分 → 随结构体一起分配
  • 是栈上变量 → 不进行堆分配

7.1.2 Small 对象分配

对于 16B ~ 32KB 的对象:

  1. 根据对象大小查表确定 size class
  2. 在 mcache 中寻找对应 class 的 span
  3. 从 span 的 allocBits 中找到空闲 slot
  4. 如果 mcache 中 span 已满,去 mcentral 交换
  5. 如果 mcentral 也没有,去 mheap 申请

7.1.3 Large 对象分配

对于 > 32KB 的大对象:量身定做 class0,直接从 mheap 上申请内存。

7.2 mcache 替换

在 mcache 中,每个 class 的 mspan 只有一个,当 mspan 满了之后,会从 mcentral 中兑换一个新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (c *mcache) refill(spc spanClass) {
// 1. 释放当前 span 回 mcentral
s := c.alloc[spc]
if s != &emptymspan {
if s.sweepgen != mheap_.sweepgen+3 {
throw("bad sweepgen in refill")
}
mheap_.central[spc].mcentral.uncacheSpan(s)
}

// 2. 从 mcentral 获取新的 span
s = mheap_.central[spc].mcentral.cacheSpan()
if s == nil {
throw("out of memory")
}

// 3. 更新 mcache
c.alloc[spc] = s
}

7.3 mcentral 扩容

mcentral 中,只有有限数量的 mspan,当 mspan 缺少时,会像 mheap 中开辟新的 heapArena,并申请对应 class 的 span。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (c *mcentral) grow() *mspan {
// 1. 计算需要的页数
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])

// 2. 从 mheap 分配新的 span
s := mheap_.alloc(npages, c.spanclass)
if s == nil {
return nil
}

// 3. 初始化 span
n := (npages << pageShift) / size
s.limit = s.base() + size*n

return s
}

7.4 mallocgc 源码分析

mallocgc 是 Go 内存分配的核心函数,核心结构如下:

1
2
3
4
5
6
mallocgc
├── mallocgcTiny // Tiny 对象 (0~16B, 无指针)
├── mallocgcSmallNoscan // Small 对象 (16B~32KB, 无指针)
├── mallocgcSmallScanNoHeader // Small 对象 (带指针, 无 header)
├── mallocgcSmallScanHeader // Small 对象 (带指针, 有 header)
└── mallocgcLarge // Large 对象 (>32KB)

源码注释如下(省略了与内存分配无关的次要代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...

// 零大小分配的快速路径
if size == 0 {
return unsafe.Pointer(&zerobase)
}

// ========== 核心分配逻辑 ==========
var x unsafe.Pointer
var elemsize uintptr

if size <= maxSmallSize-gc.MallocHeaderSize {
// 小对象分配
if typ == nil || !typ.Pointers() {
// 无指针对象
if size < maxTinySize {
x, elemsize = mallocgcTiny(size, typ) // Tiny 路径
} else {
x, elemsize = mallocgcSmallNoscan(size, typ, needzero) // Small Noscan
}
} else {
// 有指针对象(必须归零)
if !needzero {
throw("objects with pointers must be zeroed")
}
if heapBitsInSpan(size) {
x, elemsize = mallocgcSmallScanNoHeader(size, typ) // 位图在 span 中
} else {
x, elemsize = mallocgcSmallScanHeader(size, typ) // 需要 malloc header
}
}
} else {
// 大对象分配
x, elemsize = mallocgcLarge(size, typ, needzero)
}

// ...
return x
}

7.4.1 Tiny 对象分配:mallocgcTiny

用于 < 16B 且无指针的对象,多个对象合并到 16B 块中。mallocgcTiny 进行了以下优化:

  • 对齐优化:根据大小选择合适的对齐

  • 空间复用:多个对象共享 16B 块

  • 无锁快速路径:nextFreeFast 尝试无锁获取

  • 平均浪费率:约 12.5%(远低于独立分配的 87.5%)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func mallocgcTiny(size uintptr, typ *_type) (unsafe.Pointer, uintptr) {
// ========== 1. 获取 M 和 mcache ==========
mp := acquirem()
mp.mallocing = 1

c := getMCache(mp)
off := c.tinyoffset

// ========== 2. 对齐计算 ==========
if size&7 == 0 {
off = alignUp(off, 8) // 8 字节对齐
} else if goarch.PtrSize == 4 && size == 12 {
off = alignUp(off, 8) // 32 位特殊情况
} else if size&3 == 0 {
off = alignUp(off, 4) // 4 字节对齐
} else if size&1 == 0 {
off = alignUp(off, 2) // 2 字节对齐
}

// ========== 3. 尝试在现有 tiny block 中分配 ==========
if off+size <= maxTinySize && c.tiny != 0 {
x := unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x, maxTinySize
}

// ========== 4. 需要新的 tiny block ==========
span := c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, _ = c.nextFree(tinySpanClass)
}

x := unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0 // 清零前 16 字节
(*[2]uint64)(x)[1] = 0

// ========== 5. 更新 tiny allocator 状态 ==========
if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize

// ... 发布屏障和返回
publicationBarrier()
return x, size
}

7.4.2 小对象无扫描分配:mallocgcSmallNoscan

用于 16B~32KB 且无指针的对象。

关键点:

  • 查表优化:两个查找表覆盖不同大小范围

  • 延迟归零:只在需要时归零

  • GC 协作:黑色分配或设置 freeIndexForScan

  • 发布屏障:确保内存可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func mallocgcSmallNoscan(size uintptr, typ *_type, needzero bool) (unsafe.Pointer, uintptr) {
mp := acquirem()
mp.mallocing = 1

c := getMCache(mp)

// ========== 1. 确定 size class ==========
var sizeclass uint8
if size <= gc.SmallSizeMax-8 {
sizeclass = gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
} else {
sizeclass = gc.SizeToSizeClass128[divRoundUp(size-gc.SmallSizeMax, gc.LargeSizeDiv)]
}
size = uintptr(gc.SizeClassToSize[sizeclass])

// ========== 2. 从 mcache 分配 ==========
spc := makeSpanClass(sizeclass, true) // true = noscan
span := c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, checkGCTrigger = c.nextFree(spc)
}

// ========== 3. 按需归零 ==========
x := unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(x, size)
}

// ========== 4. 发布屏障 ==========
publicationBarrier()

// ========== 5. GC 期间分配黑色 ==========
if writeBarrier.enabled {
gcmarknewobject(span, uintptr(x))
} else {
span.freeIndexForScan = span.freeindex
}

// ... 返回
return x, size
}

7.4.3 小对象有扫描分配(无 Header):mallocgcSmallScanNoHeader

用于带指针的小对象,且堆位图在 span 中。

关键点:

  • 堆位图设置:heapSetTypeNoHeader 根据类型设置指针位图

  • 扫描统计:累加 scanAlloc 用于 GC 调度

  • 8字节优化:64位平台的特殊处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func mallocgcSmallScanNoHeader(size uintptr, typ *_type) (unsafe.Pointer, uintptr) {
mp := acquirem()
mp.mallocing = 1

c := getMCache(mp)

// ========== 1. 确定 size class ==========
sizeclass := gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
spc := makeSpanClass(sizeclass, false) // false = scan

// ========== 2. 分配和归零 ==========
span := c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, checkGCTrigger = c.nextFree(spc)
}
x := unsafe.Pointer(v)
if span.needzero != 0 {
memclrNoHeapPointers(x, size)
}

// ========== 3. 设置堆位图(类型信息)==========
if goarch.PtrSize == 8 && sizeclass == 1 {
// 8 字节 class 在 64 位平台已预设指针位
c.scanAlloc += 8
} else {
c.scanAlloc += heapSetTypeNoHeader(uintptr(x), size, typ, span)
}
size = uintptr(gc.SizeClassToSize[sizeclass])

// ========== 4. 发布屏障 + GC 协作 ==========
publicationBarrier()
if writeBarrier.enabled {
gcmarknewobject(span, uintptr(x))
} else {
span.freeIndexForScan = span.freeindex
}

// ... 返回
return x, size
}

7.4.4 小对象有扫描分配(有 Header):mallocgcSmallScanHeader

用于带指针的小对象,需要 malloc header 存储类型信息。

Malloc Header 设计:

1
2
3
4
5
+------------------+
| *_type (header) 丨 <- 指向类型元数据的指针
+------------------+
| 实际对象数据 | <- x 指向这里
+------------------+

为什么需要 Header:

  • 当对象较大且堆位图不在 span 中时,需要额外存储类型信息

  • Header 存储类型指针,GC 可以快速找到对象的类型信息

  • 权衡:增加少量空间换取更快的 GC 扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func mallocgcSmallScanHeader(size uintptr, typ *_type) (unsafe.Pointer, uintptr) {
mp := acquirem()
mp.mallocing = 1

c := getMCache(mp)

// ========== 1. 增加 header 空间 ==========
size += gc.MallocHeaderSize

// ========== 2. 确定 size class ==========
var sizeclass uint8
if size <= gc.SmallSizeMax-8 {
sizeclass = gc.SizeToSizeClass8[divRoundUp(size, gc.SmallSizeDiv)]
} else {
sizeclass = gc.SizeToSizeClass128[divRoundUp(size-gc.SmallSizeMax, gc.LargeSizeDiv)]
}
size = uintptr(gc.SizeClassToSize[sizeclass])

// ========== 3. 分配和归零 ==========
spc := makeSpanClass(sizeclass, false)
span := c.alloc[spc]
v := nextFreeFast(span)
if v == 0 {
v, span, checkGCTrigger = c.nextFree(spc)
}
x := unsafe.Pointer(v)
if span.needzero != 0 {
memclrNoHeapPointers(x, size)
}

// ========== 4. 设置 malloc header ==========
header := (**_type)(x)
x = add(x, gc.MallocHeaderSize) // 跳过 header
c.scanAlloc += heapSetTypeSmallHeader(uintptr(x), size-gc.MallocHeaderSize, typ, header, span)

// ========== 5. 发布屏障 + GC 协作 ==========
publicationBarrier()
if writeBarrier.enabled {
gcmarknewobject(span, uintptr(x)-gc.MallocHeaderSize)
} else {
span.freeIndexForScan = span.freeindex
}

// ... 返回
return x, size
}

7.4.5 大对象分配:mallocgcLarge

用于 > 32KB 的对象。

大对象优化:

  • 直接分配:绕过 mcache/mcentral,直接从 mheap

  • largeType 字段:避免为大对象创建复杂的位图

  • 分块归零:允许抢占,减少延迟

  • 量身定做:每个大对象有专属的 span

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func mallocgcLarge(size uintptr, typ *_type, needzero bool) (unsafe.Pointer, uintptr) {
mp := acquirem()
mp.mallocing = 1

c := getMCache(mp)

// ========== 1. 直接从 mheap 分配 ==========
span := c.allocLarge(size, typ == nil || !typ.Pointers())
span.freeindex = 1
span.allocCount = 1
span.largeType = nil // 暂时设为 nil,防止 GC 过早扫描

size = span.elemsize
x := unsafe.Pointer(span.base())

// ========== 2. 发布屏障 ==========
publicationBarrier()

// ========== 3. GC 协作 ==========
if writeBarrier.enabled {
gcmarknewobject(span, uintptr(x))
} else {
span.freeIndexForScan = span.freeindex
}

// ========== 4. 设置类型信息 ==========
if typ != nil && typ.Pointers() {
if !heapBitsInSpan(span.elemsize) {
// 大对象使用 largeType 字段
span.largeType = typ
// 发布屏障确保 largeType 可见
publicationBarrier()
} else {
c.scanAlloc += heapSetTypeLarge(uintptr(x), span.elemsize, typ, span)
}
}

// ========== 5. 按需归零(可抢占) ==========
if needzero && span.needzero != 0 {
if goexperiment.AllocHeaders {
memclrNoHeapPointersChunked(size, x) // 分块归零
} else {
memclrNoHeapPointers(x, size)
}
}

// ... 返回
return x, size
}

八、Go 1.20+ 新增特性

Go 在 1.16 之后的版本中引入了多项重要的内存管理特性,极大地增强了 Go 的能力和灵活性。

8.1 User Arena(Go 1.20+)

概述

User Arena 允许应用程序手动管理一组对象的生命周期,所有对象在同一个 arena 中分配,可以一次性释放整个 arena。

使用场景

  • 临时数据处理:请求处理完后批量释放
  • 请求级别内存池:每个请求一个 arena
  • 减少 GC 压力:大量临时对象不进入 GC 扫描

数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type mheap struct {
// mheap 中的 user arena 状态
userArena struct {
arenaHints *arenaHint // 分配提示
quarantineList mSpanList // 隔离列表(等待无指针引用)
readyList mSpanList // 就绪列表(可复用)
}
}

type mspan struct {
// mspan 中的标记
isUserArenaChunk bool // 是否为 user arena chunk
userArenaChunkFree addrRange // chunk 分配管理
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
import "arena"

func processRequest(data []byte) {
// 创建 arena
a := arena.NewArena()
defer a.Free() // 批量释放

// 在 arena 中分配对象
obj := arena.New[MyStruct](a)

// 使用对象...
}

8.2 Weak Pointer(Go 1.23+)

概述

弱引用机制允许持有对象的引用,但不阻止 GC 回收该对象。

使用场景

  • 缓存:缓存条目可以被 GC 回收
  • Observer 模式:观察者不阻止被观察对象回收
  • 循环引用打破:避免内存泄漏

数据结构

1
2
3
4
5
6
7
8
// mheap 中的弱指针映射
immortalWeakHandles immortalWeakHandleMap // 不朽对象的弱指针

// Special 记录
specialWeakHandle struct {
special special
handle *atomic.Uintptr // 弱指针句柄
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "weak"

type Cache struct {
items map[string]weak.Pointer[*Item]
}

func (c *Cache) Get(key string) *Item {
wp := c.items[key]
return wp.Value() // 可能返回 nil(已被 GC)
}

func (c *Cache) Set(key string, item *Item) {
c.items[key] = weak.Make(item)
}

实现细节

  • 弱指针本身不占用 GC 扫描时间
  • 对象被回收后,弱指针自动变为 nil
  • 弱指针转强指针需要确保 span 已清扫

8.3 Cleanup 机制(Go 1.24+)

概述

类似 finalizer 但更安全的资源清理机制,不会使对象复活

Cleanup vs Finalizer

特性 Finalizer Cleanup
对象复活 不会
执行时机 第一次变成不可达 对象真正释放前
多个回调 不支持 支持
GC 延迟 较大 较小

数据结构

1
2
3
4
5
6
7
8
9
// mheap 中的 cleanup ID
cleanupID uint64 // 全局唯一 ID 计数器

// Special 记录
specialCleanup struct {
special special
fn *funcval // 清理函数
id uint64 // 全局唯一 ID
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import "runtime"

type Resource struct {
handle uintptr
}

func NewResource() *Resource {
r := &Resource{handle: openResource()}

// 注册 cleanup(不会使 r 复活)
runtime.AddCleanup(r, func() {
closeResource(r.handle)
})

return r
}

8.4 Pinner 机制(Go 1.21+)

概述

固定对象在内存中的位置,防止 GC 移动(为未来的移动式 GC 做准备)。

使用场景

  • CGO 交互:C 代码持有 Go 对象指针
  • DMA 操作:硬件直接访问内存
  • 性能优化:避免某些热点对象移动

数据结构

1
2
3
4
5
6
7
8
// mspan 中的 pin 位图
pinnerBits *gcBits // 标记哪些对象被固定

// Special 记录(支持多次 pin)
specialPinCounter struct {
special special
counter uintptr // pin 计数
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
import "runtime"

func passToC(data []byte) {
var pinner runtime.Pinner
pinner.Pin(&data[0]) // 固定切片底层数组

// 调用 C 函数
C.processData(unsafe.Pointer(&data[0]), C.int(len(data)))

pinner.Unpin() // 解除固定
}

8.5 GreenTeaGC(实验性,Go 1.24+)

概述

实验性的新 GC 算法,旨在进一步降低延迟。

关键改进

  • Span Inline Mark Bits:将 mark bits 内联到 span 中
  • 增量标记:更细粒度的标记控制
  • 减少停顿:优化 STW 阶段

数据结构

1
2
3
4
5
// mspan 中的 GreenTeaGC 字段
scanIdx uint16 // 扫描索引

// heapArena 中的位图
pageUseSpanInlineMarkBits [pagesPerArena / 8]uint8

启用方式

1
GOEXPERIMENT=greentea go build myapp.go

九、性能优化技巧

9.1 减少内存分配

1. 复用对象(sync.Pool)

1
2
3
4
5
6
7
8
9
10
11
12
13
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}

func process() {
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()

// 使用 buf...
}

2. 预分配切片

1
2
3
4
5
6
7
8
9
10
11
// 不好
var items []Item
for i := 0; i < 1000; i++ {
items = append(items, Item{})
}

// 好
items := make([]Item, 0, 1000)
for i := 0; i < 1000; i++ {
items = append(items, Item{})
}

3. 字符串拼接优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不好
s := ""
for i := 0; i < 100; i++ {
s += "a"
}

// 好
var b strings.Builder
b.Grow(100)
for i := 0; i < 100; i++ {
b.WriteString("a")
}
s := b.String()

9.2 避免逃逸

1. 返回值而非指针

1
2
3
4
5
6
7
8
9
10
// 逃逸
func newPoint() *Point {
p := Point{x: 1, y: 2}
return &p // 逃逸
}

// 不逃逸
func newPoint() Point {
return Point{x: 1, y: 2}
}

2. 使用确定大小的数组

1
2
3
4
5
6
7
8
9
// 逃逸
func process() {
data := make([]byte, n) // 如果 n 不是常量,可能逃逸
}

// 不逃逸
func process() {
var data [1024]byte // 数组在栈上
}

9.3 检测工具

逃逸分析

1
go build -gcflags="-m" main.go

内存分析

1
2
3
4
5
6
7
8
9
import _ "net/http/pprof"

func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()

// 访问 http://localhost:6060/debug/pprof/heap
}

Trace 分析

1
2
go test -trace=trace.out
go tool trace trace.out

十、总结

10.1 内存模型演进

从 Go 1.16 到 Go 1.25.3,内存模型的主要演进方向:

  1. 更灵活的内存管理
    • User Arena:用户可控的批量分配/释放
    • 适应更多场景需求
  2. 更丰富的引用语义
    • Weak Pointer:支持弱引用
    • 打破循环引用,优化缓存
  3. 更安全的资源管理
    • Cleanup 机制:不会使对象复活
    • 减少 finalizer 带来的问题
  4. 更好的 CGO 支持
    • Pinner 机制:固定对象位置
    • 安全地与 C 代码交互
  5. 持续的 GC 优化
    • GreenTeaGC:实验性的低延迟 GC
    • Inline mark bits:减少内存开销

10.2 核心设计原则

Go 内存分配器的核心设计原则始终如一:

  1. 多层级缓存
    • 本地缓存:mcache(Per-P,无锁)
    • 中央索引:mcentral(按 size class,需要锁)
    • 全局堆:mheap(全局,需要全局锁)
    • 虚拟内存:heapArena(64MB 单元)
  2. 减少锁竞争:Per-P 缓存 + 细粒度锁
  3. 分级管理:68 个 size class 减少碎片
  4. 延迟归零:按需清零提高性能
  5. 与 GC 协作:双缓冲、sweepgen 等机制

10.3 最佳实践

  1. 理解内存分配路径:优先使用 mcache 的无锁快速路径
  2. 减少逃逸:让对象尽量在栈上分配
  3. 复用对象:使用 sync.Pool 减少分配
  4. 预分配容量:避免 slice/map 反复扩容
  5. 选择合适的特性:根据场景使用 User Arena、Weak Pointer 等

10.4 参考资料

附录:常用命令

内存相关环境变量

1
2
3
GOGC=100          # GC 触发时的堆增长百分比
GOMEMLIMIT=4GiB # 内存限制(Go 1.19+)
GODEBUG=gctrace=1 # 打印 GC 跟踪信息

性能分析

1
2
3
4
5
6
7
8
9
10
11
# CPU profile
go test -cpuprofile=cpu.prof
go tool pprof cpu.prof

# Memory profile
go test -memprofile=mem.prof
go tool pprof mem.prof

# Trace
go test -trace=trace.out
go tool trace trace.out

逃逸分析

1
2
3
4
5
# 编译时查看逃逸分析
go build -gcflags="-m -m" main.go

# 查看汇编代码
go tool compile -S main.go