本文从Go源码角度探索内存分配原理。Go的内存分配是一个演进的过程,最初借鉴于tcmalloc,这里我们直接看go 1.15.6的源码,不再探究历史。
注意:以下代码片段为了方便说明都经过不同程度的简化。
基本概念
fixalloc
一种放置大小一样对象的free-list内存分配器,下面提到的mheap、mcache等堆区管理结构都是用这个分配器分配的。
Go内存页
Go堆内存管理中以8KB作为内存页。
mheap
runtime.mheap
是内存分配器的核心组件,每个go进程只维护一个runtime.mheap
结构体。管理整个malloc堆区。
runtime/mheap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
type mheap struct {
// arenaL1Bits在linux下为0, arenaL2Bits的计算公式为:
// arenaL2Bits = heapAddrBits - logHeapArenaBytes - arenaL1Bits
// heapAddrBits就是地址空间所占字节数,在amd64上一般是48,logHeapArenaBytes是每个
// heapArena管理的内存所占字节数,64M是2^26,所以logHeapArenaBytes为26
//
// 最终对于Linux系统来说这里等于定义了arenas [1]*[1 << 22]*heapArena,完美包含48位的地址空间
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
// 中心缓存,管理mspan双向列表
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
}
|
heapArena
runtime.heapArena
是堆内存块,每个内存块管理64M内存(2^26 Bytes)。arena的作用主要是一个快速索引,能根据内存地址迅速找到所在内存页是否被用,被用的话能快速找到关联的mspan。
runtime/mheap.go
1
2
3
4
5
6
7
8
|
type heapArena struct {
// 表示管理的2^26(64M)内存的占用情况
// heapArenaBitmapBytes = heapArenaBytes / (sys.PtrSize * 4)
// 对于64位的linux,bitmap[i]管理32字节的占用情况
bitmap [heapArenaBitmapBytes]byte
// 表示这个arena中每个Go内存页对应的管理单元
spans [pagesPerArena]*mspan
}
|
mcentral
中心缓存,管理进程中所有相同spanClass的mspan。
runtime/mcentral.go
1
2
3
4
5
6
7
8
9
10
11
|
type mcentral struct {
lock mutex
spanclass spanClass
// 没有空闲对象的mspan双向链表
// 给mcache分配mspan的时候,先从这个链表扫描mspan,
// 并进行清扫标记,促进未来得及GC的mspan重复利用
nonempty mSpanList
// 有空闲对象的mspan双向链表
empty mSpanList
}
|
mspan
内存管理基本单元,每个mspan管理npages个内存页,这个内存是连续的。
runtime/mheap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
type mspan struct {
// 管理的起始地址
startAddr uintptr
// 本mspan管理多少个Go内存页
npages uintptr
// 跨度类,下面有介绍,表示对象大小等级和是否有指针
spanclass spanClass
// 对象大小等级对应的大小。由于对象大小各异,不利于快速内存分配,
// 这是一种以空间换时间的方法,做了一个权衡,下面spanclass会详细介绍
// 具体空间损耗等问题。
elemsize uintptr
// 根据对象大小等级算出的本mspan管理的对象个数
// nelems = nbytes / elemsize
nelems uintptr
// 主要用于runtime.mSpanList,在runtime.mcentral中使用来管理内存
// 前一个span
next *mspan
// 后一个span
prev *mspan
}
|
spanClass
runtime/mheap.go
1
2
3
4
5
|
// 包含了size class,和noscan标识
// size class是指go将对象大小分成67个类别
// noscan是一个标识,代表对象中是否含有指针,含有指针的需要被扫描
// spanClass = sizeclass<<1 | noscan
type spanClass uint8
|
其中size class就是对象大小等级,从0到66,0是特殊值,表示大对象(大于32KB),大对象独立存在一个mspan中,不分级。
class |
elemSize |
npages |
nelems |
tail waste |
max waste |
1 |
8 |
1 |
1024 |
0 |
87.5% |
2 |
16 |
1 |
512 |
0 |
43.75% |
3 |
32 |
1 |
256 |
0 |
46.88% |
4 |
48 |
1 |
170 |
0.39% |
31.52% |
5 |
64 |
1 |
128 |
0 |
23.44% |
… |
… |
… |
… |
… |
… |
65 |
28672 |
7 |
2 |
0 |
4.91% |
66 |
32768 |
4 |
1 |
0 |
12.50% |
我们可以看到mspan的elemSize、npages、nelems都由spanClass决定(除了大对象),其中损耗率计算公式如下:
1
2
|
# pageSize为8KB
waste=1-(actualElemSize*nelems)/(npages*pageSize)
|
细心的读者会看到class为4的时候,最小的损耗不是0,因为8192不能被48整除,有一部分空间被浪费了。
mcache
runtime.mcache
是一个和Go中P-M-G调度模型中P绑定的缓存,管理着有空闲空间的mspan,供绑定到这个P中的线程使用。
runtime/mheap.go
1
2
3
4
5
6
|
type mcache struct {
// numSpanClasses = _NumSizeClasses << 1
// 也就是线程缓存中,包含67*2个mspan
// 之所以要乘以2,是因为对象大小等级有67种,是否包含指针(noscan)有两种,所以是67*2
alloc [numSpanClasses]*mspan
}
|
堆分配内存过程
Go语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种。
类别 |
大小 |
tiny |
(0,16B) |
small |
[16B, 32KB] |
large |
(32KB, +∞) |
这三种对象的分配策略各不相同。
微对象
对于小于16字节的微对象,Go的优化分配策略是改成分配一块比较大的内存块(maxTinySize,目前是16字节),从这个内存中分配较小的字符串以及逃逸的临时变量。需要注意的是,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。微对象不能含有指针。
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 mcache struct {
// 微内存分配器的内存块起始地址
tiny uintptr
// 微内存分配器用到哪里了
tinyoffset uintptr
// 微分配器分配了多少个还没统计到memstats.tinyallocs的对象
local_tinyallocs uintptr
}
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if noscan && size < maxTinySize {
off := c.tinyoffset
if off+size <= maxTinySize && c.tiny != 0 {
// The object fits into existing tiny block.
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.local_tinyallocs++
mp.mallocing = 0
releasem(mp)
return x
}
// Allocate a new maxTinySize block.
span = c.alloc[tinySpanClass]
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// See if we need to replace the existing tiny block with the new one
// based on amount of remaining free space.
if size < c.tinyoffset || c.tiny == 0 {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
}
...
}
|
相关链接
daveness.me
文章作者
shandowc
上次更新
2020-07-15
(7a1c413)