我们知道Go运行时(Go Runtime)调度器在调度时会将 Goroutines(G)绑定到逻辑处理器(P)(Logical Processors) 运行。类似的,Go实现的TCMalloc将内存页(Memory Pages)分为67种不同大小规格的块。
如果你不熟悉Go的调度器可以先参见《 Go scheduler: Ms,Ps &Gs》, https://povilasv.me/go-scheduler/然后继续阅读,下面给大家介绍一下Go内存分配器。
如果页的规格大小为1KB那么Go管理粒度为8192B内存将被切分为8个像下图这样的块。
Go中这些页通过mspan结构体进行管理。
mspan
简单的说,mspan是一个包含页起始地址、页的span规格和页的数量的双端链表。
mcache
Go像TCMallo一样为每一个逻辑处理器(P)(LogicalProcessors)提供一个本地线程缓存(Local Thread Cache)称作mcache,所以如果Goroutine需要内存可以直接从 mcache中获取,由于在同一时间只有一个Goroutine运行在 逻辑处理器(P)(Logical Processors)上,所以中间不需要任何锁的参与。
mcache包含所有大小规格的mspan作为缓存。
由于每个P都拥有各自的mcache,所以从mcache分配内存无需持有锁。
对于每一种大小规格都有两个类型:
scan -- 包含指针的对象。
noscan -- 不包含指针的对象。
采用这种方法的好处之一就是进行垃圾回收时noscan对象无需进一步扫描是否引用其他活跃的对象。
mcache的作用是什么?
<=32k>mspan通过mcache分配
当mcache没有可用空间时会发生什么?
从mcentral的mspans列表获取一个新的所需大小规格的mspan。
mcentral
mcentral对象收集所有给定规格大小的span。每一个mcentral都包含两个mspan的列表:
empty mspanList -- 没有空闲对象或span已经被mcache缓存的span列表
nonempty mspanList -- 有空闲对象的span列表
每一个mcentral结构体都维护在mheap结构体内。
mheap
Go使用mheap对象管理堆,只有一个全局变量。持有虚拟地址空间。
就上我们从上图看到的:mheap存储了mcentral的数组。这个数组包含了各个的span的mcentral
central [numSpanClasses]struct { mcentral mcentral pad [sys.CacheLineSize unsafe.Sizeof
(mcentral{})%sys.CacheLineSize]byte}
由于我们有各个规格的span的mcentral,当一个mcache从 mcentral申请mspan时,只需要在独立的mcentral级别中使用锁,所以其它任何mcache在同一时间申请不同大小规格的 mspan将互不受影响可以正常申请。
对齐填充(Padding)用于确保mcentrals以CacheLineSize个字节数分隔,所以每一个MCentral.lock都可以获取自己的缓存行(cache line),以避免伪共享(false sharing)问题。
当mcentral列表空的时候会发生什么?mcentral从mheap获取一系列页用于需要的大小规格的span。
free[_MaxMHeapList]mSpanList:一个spanList数组。每一个 spanList 中的 mspan 包含1-127(_MaxMHeapList-1)个页。例如,free[3]是一个包含3个页的mspan链表。free表示free list,表示未分配。对应busy list。
freelarge mSpanList:一个mspan的列表。每一个元素(mspan)的页数大于127。通过mtreap结构体管理。对应busylarge。
大于32K的对象被定义为大对象,直接通过mheap分配。这些大对象的申请是以一个全局锁为代价的,因此任何给定的时间点只能同时供一个P申请。
对象分配流程
大于 32K 的大对象直接从mheap分配。
小于 16B 的使用mcache的微型分配器分配对象大小在16B-32K 之间的的,首先通过计算使用的大小规格,然后使用mcache中对应大小规格的块分配如果对应的大小规格在mcache中没有可用的块,则向mcentral 申请
如果mcentral中没有可用的块,则向mheap申请,并根据 BestFit算法找到最合适的mspan。如果申请到的mspan超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的mspan放回mheap的空闲列表。
如果mheap中没有可用span,则向操作系统申请一系列新的页(最小1MB)。
但是Go会在操作系统分配超大的页(称作arena)。分配一大批页会减少和操作系统通信的成本。
所有在堆上的内存申请都来自arena。让我们看看arena是什么。
Go虚拟内存
让我们看一个简单的Go程序的内存情况
func main() { for {}}
从上面可以即使是一个简单的程序虚拟空间占用页大概 ~100MB 左右,但是RSS仅仅占用696KB。让我们先搞清楚这之间的差异。
这里有一块内存区域大小在 ~ 2MB、64MB和32MB。这些是什么?
Arena
事实证明Go的虚拟内存布局中包含一系列arenas。初始的堆映射是一个 arena,如64MB(基于 go 1.11.5)。
所以当前内存根据我们的程序需要以小增量映射,并且初始于一个arena(~64MB)。
请首先记住这些数字。主题开始改变。早期Go需要预先保留一个连续的虚拟地址,在一个64-bit的系统 arena 的大小是 512GB。(如果分配的足够大且 被 mmap 拒绝 会发生什么?)
这些arenas就是我们所说的堆。在Go中每一个arena都以 8192B的粒度的页进行管理。
下图表示一个64MB的arena
Go同时存在其他两个块:span和bitmap。两者都在堆外分配并且包含每个arena的元数据。大多用于垃圾回收期间。
以上就是今天给大家介绍的Go内存分配器,如果你还想了解更多关于go语言的知识技巧,可以持续关注我们http://www.fastgolang.com
发表评论