图解 Go 内存管理器的内存分配策略
在 <code>Go</code> 语言里,从内存的分配到不再使用后内存的回收等等这些内存管理工作都是由 <code>Go</code> 在底层完成的。虽然开发者在写代码时不必过度关心内存从分配到回收这个过程,但是 <code>Go</code> 的内存分配策略里有不少有意思的设计,通过了解他们有助于我们自身的提高,也让我们能写出更高效的 <code>Go</code> 程序。
Go内存管理的设计旨在在并发环境中快速运行,并与垃圾回收器集成在一起。让我们看一个简单的示例:
<pre><code class="lang-go hljs">package main type smallStruct struct { a, b int64 c, d float64 }func main() { smallAllocation() } //go:noinline func smallAllocation() *smallStruct { return &smallStruct{} } 复制代码 </code></code></pre>函数上面的注释 <code>//go:noinline</code> 将禁止 <code>Go</code> 对该函数进行内联,这样 <code>main</code> 函数就会使用 <code>smallAllocation</code> 函数返回的指针变量,因为被多个函数使用,返回的这个变量将被分配到堆上。
关于内联的概念之前的文章有说过:
内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体本身。这么做的原因是它可以消除函数调用本身的开销,也使得编译器能更高效地执行其他的优化策略。
所以如果上面的例子不干预编译器的话,编译器通过内联将 <code>smallAllocation</code> 函数体里的内容直接放到 <code>main</code> 函数里,这样就不会产生<code>smallAllocation</code> 这个函数的调用了,所有的变量都是 <code>main</code>函数内这个范围使用的,也就不在需要将变量往堆上分配了。
继续说上面那个例子,通过逃逸分析命令 go tool compile -m main.go 可以确认我们上面的分析, <code>&smallStruct{}</code> 会被分配到堆上去。
<pre><code class="lang-go hljs">➜ go tool compile -m main.go main.go:12:6: can inline main main.go:10:9: &smallStruct literal escapes to heap 复制代码 </code></code></pre>借助命令 go tool compile -S main.go ,可以显示该程序的汇编代码,也可以明确地向我们展示内存的分配:
<pre><code class="lang-go hljs">0x001d 00029 (main.go:10) LEAQ type."".smallStruct(SB), AX 0x0024 00036 (main.go:10) PCDATA $2, $0 0x0024 00036 (main.go:10) MOVQ AX, (SP) 0x0028 00040 (main.go:10) CALL runtime.newobject(SB) 复制代码 </code></code></pre>内置函数 <code>newobject</code> 会通过调用另外一个内置函数 <code>mallocgc</code> 在堆上分配新内存。在Go里面有两种内存分配策略,一种适用于程序里小内存块的申请,另一种适用于大内存块的申请,大内存块指的是大于32KB。
下面我们来细聊一下这两种策略。
<h2>小于32KB内存块的分配策略</h2>当程序里发生了 <code>32kb</code> 以下的小块内存申请时,Go会从一个叫做的<code>mcache</code> 的本地缓存给程序分配内存。这个本地缓存 <code>mcache</code> 持有一系列的大小为 <code>32kb</code> 的内存块,这样的一个内存块里叫做 <code>mspan</code> ,它是要给程序分配内存时的分配单元。
从mcache中给程序分配内存
在Go的调度器模型里,每个线程 <code>M</code> 会绑定给一个处理器 <code>P</code> ,在单一粒度的时间里只能做多处理运行一个 <code>goroutine</code> ,每个 <code>P</code> 都会绑定一个上面说的本地缓存 <code>mcache</code> 。当需要进行内存分配时,当前运行的 <code>goroutine</code> 会从 <code>mcache</code> 中查找可用的 <code>mspan</code> 。从本地 <code>mcache</code>里分配内存时不需要加锁,这种分配策略效率更高。
那么有人就会问了,有的变量很小就是数字,有的却是一个复杂的结构体,申请内存时都分给他们一个 <code>mspan</code> 这样的单元会不会产生浪费。其实 <code>mcache</code> 持有的这一系列的 <code>mspan</code> 并不都是统一大小的,而是按照大小,从8字节到32KB分了大概70类的 <code>msapn</code> 。
按照大小分类的mspan
就文章开始的那个例子来说,那个结构体的大小是32字节,正好32字节的这种 <code>mspan</code> 能满足需求,那么分配内存的时候就会给它分配一个32字节大小的 <code>mspan</code> 。
alloc 分配内存
现在,我们可能会好奇,如果分配内存时 <code>mcachce</code> 里没有空闲的32字节的 <code>mspan</code> 了该怎么办? <code>Go</code> 里还为每种类别的 <code>mspan</code> 维护着一个 <code>mcentral</code> 。
<code>mcentral</code> 的作用是为所有 <code>mcache</code> 提供切分好的 <code>mspan</code> 资源。每个 <code>central</code> 会持有一种特定大小的全局 <code>mspan</code> 列表,包括已分配出去的和未分配出去的。每个 <code>mcentral</code> 对应一种 <code>mspan</code> ,当工作线程的 <code>mcache</code> 中没有合适(也就是特定大小的)的 <code>mspan</code> 时就会从 <code>mcentral</code> 去获取。 <code>mcentral</code> 被所有的工作线程共同享有,存在多个 <code>goroutine</code> 竞争的情况,因此从 <code>mcentral</code> 获取资源时需要加锁。
<code>mcentral</code> 的定义如下:
<pre><code class="lang-go hljs">//runtime/mcentral.go type mcentral struct { // 互斥锁 lock mutex // 规格 sizeclass int32 // 尚有空闲object的mspan链表 nonempty mSpanList // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表 empty mSpanList // 已累计分配的对象个数 nmalloc uint64 } 复制代码 </code></code></pre><code>mcentral</code> 里维护着两个双向链表, nonempty 表示链表里还有空闲的 <code>mspan</code> 待分配。 empty表示这条链表里的 <code>mspan</code> 都被分配了 <code>object</code> 。
mcentral
如果上面我们那个程序申请内存的时候, <code>mcache</code> 里已经没有合适的空闲 <code>mspan</code> 了,那么工作线程就会像下图这样去 <code>mcentral</code> 里去申请。
简单说下 <code>mcache</code> 从 <code>mcentral</code> 获取和归还 <code>mspan</code> 的流程:
<ul><li>nonempty mspan nonempty mspan empty mspan
</li> <li>mspan empty mspan nonempty
</li> </ul>从mcentral里申请mspan
当 <code>mcentral</code> 没有空闲的 <code>mspan</code> 时,会向 <code>mheap</code> 申请。而 <code>mheap</code>没有资源时,会向操作系统申请新内存。 <code>mheap</code> 主要用于大对象的内存分配,以及管理未切割的 <code>mspan</code> ,用于给 <code>mcentral</code>切割成小对象。
从heap上申请内存
同时我们也看到, <code>mheap</code> 中含有所有规格的 <code>mcentral</code> ,所以,当一个 <code>mcache</code> 从 <code>mcentral</code> 申请 <code>mspan</code> 时,只需要在独立的 <code>mcentral</code> 中使用锁,并不会影响申请其他规格的 <code>mspan</code> 。
上面说了每种尺寸的 <code>mspan</code> 都有一个全局的列表存放在 <code>mcentral</code>里供所有线程使用,所有 <code>mcentral</code> 的集合则是存放于 <code>mheap</code> 中的。<code>mheap</code> 里的 <code>arena</code> 区域是真正的堆区,运行时会将 <code>8KB</code> 看做一页,这些内存页中存储了所有在堆上初始化的对象。运行时使用二维的 runtime.heapArena 数组管理所有的内存,每个 runtime.heapArena 都会管理 64MB 的内存。
如果 <code>arena</code> 区域没有足够的空间,会调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存。
<h2>大于32KB内存块的分配策略</h2>Go没法使用工作线程的本地缓存 <code>mcache</code> 和全局中心缓存 <code>mcentral</code> 上管理超过32KB的内存分配,所以对于那些超过32KB的内存申请,会直接从堆上( <code>mheap</code> )上分配对应的数量的内存页(每页大小是8KB)给程序。
直接从堆上分配内存
<h2>总结</h2>我们把内存分配管理涉及的所有概念串起来,可以勾画出Go内存管理的一个全局视图:
Go内存分配的全局示意图
Go语言的内存分配非常复杂,这个文章从一个比较粗的角度来看Go的内存分配,并没有深入细节。一般而言,了解它的原理,到这个程度也就可以了(应付面试)。
总结起来关于Go内存分配管理的策略有如下几点:
<ul><li>Go在程序启动时,会向操作系统申请一大块内存,由 <code>mheap</code> 结构全局管理。
</li> <li>mspan mspan object
</li> <li>mcache mcentral mheap Go mcache mspan mcentral mspan mheap Go
</li> <li>一般小对象通过 <code>mspan</code> 分配内存;大对象则直接由 <code>mheap</code> 分配内存。
</li> </ul><h3>相关阅读</h3>Go内存管理之代码的逃逸分析
上周并发题的解题思路以及介绍Go语言调度器
<h3>参考链接</h3>Memory Management and Allocation <sup>[1]</sup>
图解Go语言内存分配 <sup>[2]</sup>
内存分配器 <sup>[3]</sup>
<h3>参考资料</h3>[1]
Memory Management and Allocation: medium.com/a-journey-w…
[2]
图解Go语言内存分配: juejin.im/post/684490…
[3]
内存分配器: draveness.me/golang/docs…
<ul><li>END -</li> </ul> 到此这篇关于“ 图解 Go 内存管理器的内存分配策略”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!您可能感兴趣的文章:
图解 Go 内存管理器的内存分配策略
mongodb的NUMA问题的解决方法
带你领略Go源码的魅力----Go内存原理详解
详解Go逃逸分析
Go:g0,特殊的 Goroutine
Goroutine的调度分析(一)
内存池原理详解
我是如何在大型代码库上使用pprof探索Go中的内存泄漏
Go内存模型
【golang源码分析】内存管理和gc原理