堆内存
1.golang堆内存分配采用和tcmalloc内存分配器类似的算法
2.堆内存划分为一个个arena空间,arena的初始地址记录在arenaBaseOffset中,在amd64架构的linux中,其值默认为64M,每个arena中有8192个page,每个page有8KB。
3.golang将内存默认分为68种大小规格,最小为8B,最大为32KB,大于32的独立分给一种类型(0),同一种规格又区分为可扫描和不可扫描(标量和指针),所有总共有136种mspan。
4.一个arena划分为多个span,一个span包含1到多个page,并固定划分为某种规格的内存块。
mheap
mheap用于管理整个堆内存,一个arena对应一个heapArena结构,一个span对应一个mspan结构。通过它们可以知道某个内存块是否已分配;已分配的内存用作指针还是标量;是否已被GC标记;是否等待清扫等信息.
central
mheap中有一个全局的mspan管理中心---mheap.central,是一个长度为136(68*2)的数组,数组结构是一个mcentral结构 padding,其作用是为了方便取用各种规格的mspan。
mcentral
一个mcentral对应一种mspan类型,记录在spanclass中,spanclass的结构:
full和partial分别表示已用尽和未用尽,而每个结构里面包含两个并发安全的spanSet,分别表示已清扫和未清扫。
为了降低多个P之间的竞争,所有对象都会有自己的本地小对象缓存mcache。
mcache中存在tiny和alloc结构,tiny用于分配小于16B的对象,alloc是一个长度为136的数组,数组元素是mspan结构。当本地mspan没有空余的时候,回去向全局的mcentral中申请(partial)新的mspan。将已经用尽的mspan替换到full中。
class对应表格
代码语言:javascript复制// class bytes/obj bytes/span objects waste bytes
// 1 8 8192 1024 0
// 2 16 8192 512 0
// 3 32 8192 256 0
// 4 48 8192 170 32
// 5 64 8192 128 0
// 6 80 8192 102 32
// 7 96 8192 85 32
// 8 112 8192 73 16
// 9 128 8192 64 0
// 10 144 8192 56 128
// 11 160 8192 51 32
// 12 176 8192 46 96
// 13 192 8192 42 128
// 14 208 8192 39 80
// 15 224 8192 36 128
// 16 240 8192 34 32
// 17 256 8192 32 0
// 18 288 8192 28 128
// 19 320 8192 25 192
// 20 352 8192 23 96
// 21 384 8192 21 128
// 22 416 8192 19 288
// 23 448 8192 18 128
// 24 480 8192 17 32
// 25 512 8192 16 0
// 26 576 8192 14 128
// 27 640 8192 12 512
// 28 704 8192 11 448
// 29 768 8192 10 512
// 30 896 8192 9 128
// 31 1024 8192 8 0
// 32 1152 8192 7 128
// 33 1280 8192 6 512
// 34 1408 16384 11 896
// 35 1536 8192 5 512
// 36 1792 16384 9 256
// 37 2048 8192 4 0
// 38 2304 16384 7 256
// 39 2688 8192 3 128
// 40 3072 24576 8 0
// 41 3200 16384 5 384
// 42 3456 24576 7 384
// 43 4096 8192 2 0
// 44 4864 24576 5 256
// 45 5376 16384 3 256
// 46 6144 24576 4 0
// 47 6528 32768 5 128
// 48 6784 40960 6 256
// 49 6912 49152 7 768
// 50 8192 8192 1 0
// 51 9472 57344 6 512
// 52 9728 49152 5 512
// 53 10240 40960 4 0
// 54 10880 32768 3 128
// 55 12288 24576 2 0
// 56 13568 40960 3 256
// 57 14336 57344 4 0
// 58 16384 16384 1 0
// 59 18432 73728 4 0
// 60 19072 57344 3 128
// 61 20480 40960 2 0
// 62 21760 65536 3 256
// 63 24576 24576 1 0
// 64 27264 81920 3 128
// 65 28672 57344 2 0
// 66 32768 32768 1 0
- class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
- bytes/obj:该class代表对象的字节数
- bytes/span:每个span占用堆的字节数,也即页数*页大小
- objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)
- waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)
超过32K大小的对象class为0。
HeapArena结构
一个arena对应一个heapArena结构
代码语言:javascript复制type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
zeroedBase uintptr
}
bitmap
用一字节标记arena中4个指针大小的内存空间:低4位用于标记指针/标量;高4位用于标记扫描/终止(后续单元是否包含指针)
spans
大小为8192,每一个index对应一个page,用于确定某一个Page对应的mspan是什么
pageInUse
长度为1024字节(8192位),标记处于使用状态的span的第一个page。(通过它可以知道有几个span,每个span的页数)
pageMarks
标记每个span的第一个page,在GC标记阶段会修改这个位图,标记哪些span中存在被标记的对象;在GC清扫阶段会根据这个位图,来释放不含标记对象的span。
mspan
一个span对应一个mspan结构,用于管理span中的一组连续的page。
代码语言:javascript复制type mspan struct {
next *mspan
prev *mspan
....
freeindex uintptr // 下一个空闲的内存块地址
nelems uintptr // 当前mspan的内存块个数
....
// allocBits is a bitmap of objects in this span.
// If n >= freeindex and allocBits[n/8] & (1<<(n%8)) is 0
// then object n is free;
allocBits *gcBits(unit8) // 标记哪些内存块被使用(分配)了
gcmarkBits *gcBits(unit8) // gc标记位图
....
spanclass spanClass // 同mcentral,当前span的数据格式
state mSpanStateBox // 表示此mspan的类型 1表示堆内存,2表示栈内存
elemsize uintptr // class表中的对象大小,也即块大小
....
}
allocBits:是一个uint8类型,用位图标记哪些内存已经被分配使用了(他是数组的首地址)。
mallocgc函数
1.辅助GC
当GC标记速率小于堆内存申请速率时,会要求当前Go携程执行辅助GC工作,每次执行至少标记64KB的内存。辅助标记的内存大小会成为信用额度,后面在申请小于该内存时,不会再执行辅助GC。 对于特殊Go携程,可以窃取全局的信用额度,而逃避辅助GC。
2.空间分配
tiny(小于16kB && noscan) 使用P自身的tiny分配,不够向mcache拿,在没有向mcentral中再拿
normal([16kB,32KB]) mspan分配
large (> 32kB) 直接向mheap(堆内存)申请对应页数page
3.位图标记 如何通过堆内存地址(p),找到对应的mspan进行标记回收?
1.amd64上面linux最多有4096个arena,寻找p在第几个arena?
arenaBaseOffset是arena的起始地址,heapArenaBytes是每个arena的大小。
即 arenaIndex = (p-arenaBaseOffset) / heapArenaBytes
2.如何确定当前p属于哪个page?
找到arena之后,需要确定当前p属于arena中哪一页。
pagesPerArena是每个arena中拥有的page数量,pageSize每一页的大小
pageIndex = (p / pageSize) % pagesPerArena
4.收尾工作
如果处于GC标记阶段,就需要对新分配的对象进行标记(GC屏障机制),如果达到GC触发条件,还需要执行一次GC标记。
栈内存
栈内存同堆内存相似,栈内存也使用mspan来管理内存,只是mspan.state来区分此mspan是栈还是堆。
栈内存在初始化时会使用两个全局的栈分配对象:stackpool 和 stackLarge。
小于32KB的内存由stackpool进行分配
>=32KB的内存由stackLarge进行分配
stackpool
代码语言:javascript复制var stackpool [_NumStackOrders]struct {
item stackpoolItem
_ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}
type stackpoolItem struct {
mu mutex
span mSpanList
}
stackpool在linux被分成 2KB 4KB 8KB 16KB 的数组(2的幂次方)
stackLarge
代码语言:javascript复制var stackLarge struct {
lock mutex
free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages)
}
stackLarge中free 是一个[25]mSpanList的数组,他的第0和1位是不会使用的,从8KB开始,下一位是上一位的两倍,是为了方便使用 log2(index)=pageNum。
分配顺序和堆内存相似
<32KB时 本地缓存(P)的 stackcache -> 全局的stackpool ->全局堆内存中申请
>=32KB时 计算所需要的Pgae数目,然后利用上诉公式,在stackLarge中找到对应的index,获取一个mspan
栈增长
栈增长是成倍增长,基础为2KB,每次增长为前一次的两倍,同时将状态置为_Gcopystack,然后调用 copystack 将原本数据复制到新的栈空间,再释放旧的栈空间。
栈收缩
栈收缩发生在GC阶段,最小只会缩到2KB(栈初始大小),可以安全收缩时,则会马上执行栈收缩,否则会设置栈收缩标记:preemptShrink = true,在携程让出cpu时,会检测此参数,进行栈收缩操作。
栈释放
栈释放发生在协程栈运行结束的时候。
>=32KB的栈,如果在GC清扫结算,直接返回给堆内存,否则归还给stackLarge
<32KB的栈,优先归还本地stackcache,如果本地满了归还全局stackpool,再满了就归还到全局堆内存中
引用:
https://www.bilibili.com/video/BV1av411G7pB?spm_id_from=333.999.0.0
https://www.bookstack.cn/read/GoExpertProgramming/chapter04-4.1-memory_alloc.md