内存分配
Golang在程序启动时预先向操作系统申请内存,包括:
arena
即堆区,应用中需要的内存都从这里分配,大小为512G- 为了方便内存的管理,arena区内存被分成一个个的
page
,页, 每个页大小为8KB
, 整个arena共512G / 8K = 64M
个页
- 为了方便内存的管理,arena区内存被分成一个个的
bitmap
即位图区,存放内存管理中所有的位图信息- 16G?
spans
即所有已创建的span的指针列表(下文会介绍span的概念和功能)- 每个Span指向一个page,故spans大小为
64M * 8B = 512MB
; (一个指针8个bytes)
- 每个Span指向一个page,故spans大小为
Span
Span作为一个数据实体,对内存Class进行管理
Class
Go为了方便内存管理,根据内存块block
的大小,分为不同的class
:
// GO1.14.4 src/runtime/sizeclasses.go
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
...
// 66 32768 32768 1 0 12.50%
以class=10
为例:
class
表示内存块类型ID: 10bytes/obj
表示每个内存块大小:144
个字节bytes/span
表示对应span的大小:8192
个字节,span以内存页为单位,一页大小8KBobjects
该span拥有内存块个数:8192 / 144 ~= 56
waste
浪费了多少字节: 整除后剩余128个字节
Span对Class的抽象
每一个Span数据结构,即是对一个Class类型的抽象
// GO1.14.4 src/runtime/mheap.go
type mspan struct {
spanclass spanClass // 当前Span对应Class的编号,如10
startAddr uintptr // span对应的起始地址
npages uintptr // span包含多少个Page(固定值,如class=10时,npages=1)
nelems uintptr // span中包含多少个块
allocCount uint16 // 已分配块个数
elemsize uintptr // 块大小
allocBits *gcBits // 已分配块的位图;gcBits是一个指针,指向该span各个块使用位图的指针
gcmarkBits *gcBits // 用于GC
next *mspan // 在span列表中下一个的span
prev *mspan // 在span列表中前一个的Span
...
}
已Class = 10为例:
Span10 = mspan{
spanclass: 10,
startAddr: pointer, // 一个指向arena的指针
npages: 1,
nelems: 56,
allocCount: 22, // 假设有22个块已分配
elemsize: 144,
...
}
可以看出,Span对应某个特定ClassID,可以提供该ClassID对应块大小内存的分配
Central
Central用于全局管理Span,当线程中内存不足时,可以想Central申请Span
Central的数据结构
// runtime/mcentral.go
type mcentral struct {
lock mutex // 并发锁,当多个线程同时来申请资源时需要加锁
spanclass spanClass // classID
nonempty mSpanList // objects非空,可申请的Span列表
empty mSpanList //
nmalloc uint64 // 累计分配对象个数
}
// 链表的方式记录span列表
type mSpanList struct {
first *mspan
last *mspan
}
可以看到,Central统一管理指定ClassID的Span列表
线程向Central申请Span步骤
- 加锁
- 从
nonempty
中获取一个可用Span,并将其从链表中删除 - 将取出的Span添加进
empty
列表,表示该Span已被申请 - 将Span返回给线程
- 解锁
Cache
为了解决多线程向Central申请Span的并发问题,Golang提供了Cache
机制,将Span缓存到Processor中,
Cache的数据结构
// Per-thread (in Go, per-P) cache for small objects.
// No locking needed because it is per-thread (per-P).
// 每个线程(或者说每个P)独有的内存缓存池
type mcache struct {
alloc [numSpanClasses]*mspan // numSpanClasses = 134, 可以根据ClassID索引得到可用的span结构
}
- Cache是线程/P独有的内存缓存,解决多线程同时向Central申请内存加锁的问题
- Cache维护Span列表,可通过ClassID作为索引快速获取可用Span
- 列表大小是67 * 2, 每个ClassID相邻两个Span,一个用于GC
Heap
上面提到的Span
和Central
都是指定某一个ClassID的数据结构,而Heap
则是管理所有ClassID的全部内存
Heap的数据结构
type mheap struct {
lock mutex
allspans []*mspan // 所有已创建的Span列表
// 维护所有Central的映射
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
// heap当前正在使用的arena,包括起止地址
curArena struct {
base, end uintptr
}
...
}