go的内存管理是由标准库自动完成的从内存管理到不再使用的情况,尽管开发人员不需要去处理它,go的底层管理经过了良好的优化并且充满了有趣的概念
go的内存分配器
相关结构为:
fixalloc: 用于固定大小的堆外对象自由列表分配器
mheap:内存堆,以页面8192粒度进行管理
mspan:由mheap管理的一系列页面
mcentral:手动给定大小类的所有跨度
mcache:具有可用空间mspans每个P缓存
mstats:分配统计信息
基本策略
1、每次从操作系统申请一大块的内存以减少系统调用
2、将申请到大块内存按照预先的切分成小块构成链表
3、为对象分配内存 只需要从大小合适的链表提取一个小块就可
4、回收帝乡内存时,将该小块内存重现归还到原链表,仪表复用
5、闲置内存过多把一部分内存给操作吸引,降低整体的开销
虚拟的内存布局
堆由一系列的arena组成,这些在64位上位64mb在32位位4mb每一个arena开始地址和他们的大小是对齐的。
每一个arena都有一个关联的heapArea对象,该对象存储一些元数据所有对象的堆位图
由于areana是对齐的,因此可以将地址视为一系列的areana帧,这些为一个地址,对于地址为nil的 不受go堆支持,arena map 为L1 map和L2
组成的两极数组。
堆上分配
go的内存管理被设计成非常快的在并发环境与垃圾收集器集成在一起。举个例子:
package main
type smallStruct struct {
a, b int64
c, d float64
}
func main() {
smallAllocation()
}
//go:noinline
func smallAllocation() *smallStruct {
return &smallStruct{}
}
这个go:noinline
将不会被内联将通过删除函数来优化,因此,最终将不进行分配。
运行这个降本分析命令go tool compile "m" main.go
将会去验证go的分配模式
main.go:14:9: &smallStruct literal escapes to heap
由于go tool compile-s main.go,转储此程序的程序集代码还将显式显示分配:
0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX
0x0024 00036 (main.go:14) PCDATA $0, $0
0x0024 00036 (main.go:14) MOVQ AX, (SP)
0x0028 00040 (main.go:14) CALL runtime.newobject(SB)
newobject函数是用于新分配和代理mallocgc的内置函数,该函数在堆上管理这些分配和代理。Go中有两种策略,一种是针对小的分配,另一种是针对大的分配。
小分配
1、将变为小尺度类别之一,然后在p的mcachae中查找相应的mspan,扫描msapn的空闲插槽,分配它,可用在不获取锁的情况下完成
2、如果span没有可用的插槽,获取UI个新的msapn,会从mcentral所需大小的msapn列表中获取可用空间的类,得到整个跨度会分销锁定中心成本。
3、如果mcentral的mspan列表为空,获得一个centra来自mheap的页面
4、如果mheap卫康或没有足够大的页面圆形,将会从操作系统中分配一组页 至少是1mb, 去操作系统分配大量的页
对于32kb以下的小分配,go将尝试从名为mcache的本地缓存获取内存。此缓存处理一个名为mspan的SPAN列表,其中包含可用于分配的内存:
每个线程m被分配给一个处理器p,并且一次最多处理一个goroutine。在分配内存时,我们当前的goroutine将使用其当前m的本地缓存来查找SPAN列表中的第一个可用对象。使用此本地缓存不需要锁定,并使分配更有效
span列表分为大约70个大小类,从8字节到32k字节,可以存储不同的对象大小:
每个span存在两次:一个包含不包含指针的对象的列表,另一个包含指针。这种区别将使垃圾收集器的使用寿命变得更容易,因为它不必扫描不包含任何指针的范围
在我们前面的示例中,结构的大小为32字节,将适合32字节的范围
现在,我们可能想知道,如果在分配期间SPAN没有空闲插槽,会发生什么情况。go维护每个大小的跨度类(称为mcentral)的中心列表,其中包含自由对象的跨度和不包含自由对象的跨度:
mcentral维护一个span双链表,每个span独有对上一个span和下一个span的引用,一个span在空的列表中的包含着一些已使用的内存。事实上,当垃圾收集器清理一部分内存时,它可以清理一部分空间,这些空间将被放回已清理这些空闲的空槽列表中
如果插槽用完,我们的程序现在可以从中央列表中请求一个span:
如果这个空的列表中没有可用的span,就需要去中心列表申请一个新的span,新的span将从堆中分配并链接到中心列表:
堆在需要时从操作系统中提取内存。如果需要更多的内存,堆将分配一大块称为arena的内存,64位体系结构为64MB,其他大部分体系结构为4MB。竞技场还将内存页映射为span:
大分配
go不使用本地缓存管理大型分配。这些大于32KB的分配被舍入到页面大小,页面直接分配给堆。
完整分配过程如下:
参考:https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44