- 内存分配
- Span
- Class
- Span对Class的抽象
- Central
- Central的数据结构
- 线程向Central申请Span步骤
- Cache
- Cache的数据结构
- Heap
- Heap的数据结构
Golang在程序启动时预先向操作系统申请内存,包括:
- arena即堆区,应用中需要的内存都从这里分配,大小为512G
- 为了方便内存的管理,arena区内存被分成一个个的page,页, 每个页大小为8KB, 整个arena共512G / 8K = 64M个页
- bitmap即位图区,存放内存管理中所有的位图信息
- 16G?
- spans即所有已创建的span的指针列表(下文会介绍span的概念和功能)
- 每个Span指向一个page,故spans大小为64M * 8B = 512MB; (一个指针8个bytes)
Span作为一个数据实体,对内存Class进行管理
ClassGo为了方便内存管理,根据内存块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: 10
- bytes/obj表示每个内存块大小: 144个字节
- bytes/span表示对应span的大小:8192个字节,span以内存页为单位,一页大小8KB
- objects该span拥有内存块个数: 8192 / 144 ~= 56
- waste浪费了多少字节: 整除后剩余128个字节
每一个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对应块大小内存的分配
CentralCentral用于全局管理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返回给线程
- 解锁
为了解决多线程向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
上面提到的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
}
...
}



