Go内存分配器

 go教练   2019-08-09 15:35   131 人阅读  0 条评论

我们知道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内存分配器

 微信截图_20190809103022.png

如果页的规格大小为1KB那么Go管理粒度为8192B内存将被切分为8个像下图这样的块。

 微信截图_20190809103031.png

Go中这些页通过mspan结构体进行管理。

mspan

简单的说,mspan是一个包含页起始地址、页的span规格和页的数量的双端链表。

 微信截图_20190809103044.png

mcache

Go像TCMallo一样为每一个逻辑处理器(P)(LogicalProcessors)提供一个本地线程缓存(Local Thread Cache)称作mcache,所以如果Goroutine需要内存可以直接从 mcache中获取,由于在同一时间只有一个Goroutine运行在 逻辑处理器(P)(Logical Processors)上,所以中间不需要任何锁的参与。

mcache包含所有大小规格的mspan作为缓存。

 微信截图_20190809103057.png

由于每个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列表

 微信截图_20190809103108.png

每一个mcentral结构体都维护在mheap结构体内。

mheap

Go使用mheap对象管理堆,只有一个全局变量。持有虚拟地址空间。

 

就上我们从上图看到的:mheap存储了mcentral的数组。这个数组包含了各个的span的mcentral

central [numSpanClasses]struct { mcentral mcentral pad [sys.CacheLineSize unsafe.Sizeof

 微信截图_20190809103127.png

(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 {}}

 微信截图_20190809103147.png

从上面可以即使是一个简单的程序虚拟空间占用页大概 ~100MB 左右,但是RSS仅仅占用696KB。让我们先搞清楚这之间的差异。

 微信截图_20190809103158.png

这里有一块内存区域大小在 ~ 2MB、64MB和32MB。这些是什么?

Arena

事实证明Go的虚拟内存布局中包含一系列arenas。初始的堆映射是一个 arena,如64MB(基于 go 1.11.5)。

 微信截图_20190809103212.png

所以当前内存根据我们的程序需要以小增量映射,并且初始于一个arena(~64MB)。

请首先记住这些数字。主题开始改变。早期Go需要预先保留一个连续的虚拟地址,在一个64-bit的系统 arena 的大小是 512GB。(如果分配的足够大且 被 mmap 拒绝 会发生什么?)

这些arenas就是我们所说的堆。在Go中每一个arena都以 8192B的粒度的页进行管理。

下图表示一个64MB的arena

 微信截图_20190809103236.png

Go同时存在其他两个块:span和bitmap。两者都在堆外分配并且包含每个arena的元数据。大多用于垃圾回收期间。

以上就是今天给大家介绍的Go内存分配器如果你还想了解更多关于go语言的知识技巧,可以持续关注我们http://www.fastgolang.com

本文地址:http://fastgolang.com/136.html
版权声明:本文为原创文章,版权归 go教练 所有,欢迎分享本文,转载请保留出处!
NEXT:已经是最新一篇了

 发表评论


表情

还没有留言,还不快点抢沙发?