golang的内存分配思想从 tcmalloc 而来,思路是把对象分配成小对象减少锁的力度或无锁增加效率
定义
golang 内部的页(Page)大小为 8B
空间大小
golang 内部把要申请或使用的空间大小分为了三大类:微对象 (<16B),小对象(16B~32KB),大对象(>32KB), 其中小对象又分为 67 种,定义在src/runtime/sizeclasses.go
中,在实际分配时候会用向上取整的方式寻找最合适的大小,第 0 种代表是大对象
mspan
mspan(源码src/runtime/mheap.go)是包含了 n 个页的集合,内部有两个字段startAddr
和npages
,分别代表 page 的开始地址,和一共几个 page. 还有spanclass
字段表示该 mspan 是哪种对象的集合(上述定义的 67 种的一种)
mcache
mcache (源码src/runtime/mcache.go)是绑定在GMP
模型中 P 上的缓存对象,里面存放了136 种对象(672 下标是 0 是大对象,这里便于计算 加上了 0,用了 682 个),分别代表 67 个对象的分配,其中*2 代表了对象内是否包含指针,用于优化 gc
mcentral
mecntral(源码src/runtime/mcentral.go)也是代表了一种对象,但是保存了从 mheap 申请的内存块,包括带指针和不带指针的, 67 种类型.每一种类型对应一个 mcentral,也就是一种类型一个锁
mheap
mheap(源码src/runtime/mheap.go)保存了全局的对象,从内存分配的对象先由 mheap 托管,同时,大文件(>32KB)文件也是直接从 mheap 申请,拥有全局锁
预申请
golang 在 64 位的操作系统上会预先申请一整块内存用于分配
arena
就是所说的内存分配区域,所有的页都保存在 arena
区域,大小为 512GB,一个页8B,所以一次申请了512GB/8B = 6410241024*1024 个page
bitmap
每个 page 在 bitmap 中用两个位标记对应地址中是否存在对象,另外是标记此对象是否被gc标记过,所以一共占用 512GB/4*8B = 16GB 大小的空间
spans
里面存放了 mspan 的集合,每个 mspan 里面存放 n 个page ,span 主要是一种大小的数据,这个主要是根据 golang 内定的 67 种普通大小的 span,每种类型的 span 内部 page 大小不一定,比如第一种大小是 8B,那么里面就存放了 1 个 page,这个 span 就可以分配 1024 个8B 大小的空间,这里也没啥公式,在源码 src/runtime/sizeclasses.go
,可以查到所有的 span 类型,spans 一共占用了 512MB 的空间,但有一点,span 内部的 page 一定是连续的
分配
在 goroutine 在运行时进行内存申请,分配器会先判断是什么类型的对象
case 微对象:
微对象会用 tinymalloc 微对象分配器,同时会对对象进行内存对齐,比如 3B 的对象分配 4B,7B 的对象分配 8B,这块在源码有,就不详说了,如果空间不够就会一层一层向上找,同小对象
case 大对象:
大对象直接从 mheap 上申请,同时由于可能并发,所以一定是加锁获取的
case 小对象:
小对象在 golang 中最常见,所以,分配规则也是最复杂的 首先去 mcache 中申请,由于 mcache 是绑定在 P (GMP) 上的,所以不会发生竞争,这块不用加锁,分配器根据申请的对象大小,向上取整,找到对应的 67 种类型的一种,然后返回. 当 mcache 中的该类型内存不够分配时,分配器去先去mcentral对应的类型下的 mspan 申请,然后放到P 的mcache 中,这个过程由于可能多个 goroutine 竞争,所以会有锁 当该类型的 mcentral 中的空间也不够时,分配器会去 mheap中申请 mheap 最后给 goroutine 分配 最后如果 mheap 中的内存也不够了,那么这时候,就需要重新去操作系统申请了(mmap),操作系统也么有就会 oom
参考
10分钟掌握golang内存管理机制
详解Go语言的内存模型及堆的分配管理
图解Go语言内存分配
go需要自己管理内存吗,为什么面试都爱问内存管理呢? - 小徐先生的回答 - 知乎
Golang源码探索(三) GC的实现原理