go 的内存管理原理
参考tcmalloc来进行,本质上就是一个内存池,只不过内部做了许多优化,比如自动伸缩内存池大小,合理的切割内存块等
内存池 mheap:
Go程序在启动之初,会一次性从操作系统那里申请一大块内存作为内存池。这块内存空间会放在一个叫做mheap的struct中管理,mheap负责将这一整块内存切割成不同的区域,并将其中一部分内存切割成合适的大小,分配给用户使用。
page: 内存页,一块8k大小的内存空间。Go与操作系统之间的内存申请和释放,都是以page为单位的。
span:内存块,一个或多个连续的page组成一个span。
sizeclass: 空间规格,每个span都带有一个sizeclass,标记着该span中的page应该如何使用。
object:对象,用来存储一个变量数据内存空间,一个span在初始化时,会被切割成一堆等大的object。假设object的大小是16B,span的大小为8K,那么就会把span中的page初始化为512个object。所谓内存分配,就是分配一个object。
不同的颜色代表不同的span,不同的span的sizeclass不同,表示里面的page将会按照不同的规格切割成等大object用作分配。
内部的整体布局:
mheap.spans:用存储page和span信息,比如一个span的起始地址是多少,由几个page,已经使用了多少等。
mheap.bitmap: 存储着各个span中对象的标记信息,比如对象是否可以回收等。
mheap.arena_start:将要分配给应用程序使用的空间。
图中的空间大小,是 Go 向操作系统申请的虚拟内存地址空间,操作系统会将该段地址空间预留出来不做它用;而不是真的创建出这么大的虚拟内存,在页表中创建出这么大的映射关系。
mcentral:
用途相同的span会以链表的形式组织在一起。这里的用途用sizeclass表示,就是指该span用来存储哪种大小的对象。比如分配一块大小为n的内存时,系统计算n应该使用哪种sizeclass,然后根据sizeclass的值去找到一个可用的span来用作分配。
找到合适的span后,会从中取出一个object返回给上层使用,这些span被放在一个叫做mcentrral的结构中管理。
mheap将从OS那里申请过来的内从初始化为一个大span(sizeclass = 0),然后根据需要从这个大span中切出小span,放在mcentral中管理。大span由mheap.freelarge和mheap.busylarge等进行管理。如果mecntral中的span不够用了,会从mheap.freelarge上再切一块,如果mheap.freelarge空间不够,会再次从OS那里申请内存。
这种方式可以避免出现内存碎片,因为同一个span时按照固定大小分配和回收的,不会出现不可利用的一小块内存把内存分割。
mcache:
每一个cache和每一个处理器§是一一对应的,也就是说每一个P都有一个mcache成员。Goroutine申请内存时,首先从其所在的P的mcache中分配,如果mcache没有可用的span,再从mcentral中获取,并填充到mcache中
从mcache上分配内存空间不需要加锁,因为在同一时间,一个P只有一个线程在上边运行,不可能出现竞争。
zero size:
对于一些所需内存大小为0的对象,比如[0]int
,struct{}
等本就不需要分配内存,所以系统会之间返回一个固定的内存地址
func mallocgc(size uintptr, typ *_type, flags uint32) unsafe.Pointer {
// 申请的 0 大小空间的内存
if size == 0 {
return unsafe.Pointer(&zerobase)
}
//.....
}
总结
1、内存分配大多时候都是在用户态完成的,不需要频繁进入内核态。
2、每个 P 都有独立的 span cache,多个 CPU 不会并发读写同一块内存,进而减少 CPU L1 cache 的 cacheline 出现 dirty 情况,增大 cpu cache 命中率。
3、内存碎片的问题,Go 是自己在用户态管理的,在 OS 层面看是没有碎片的,使得操作系统层面对碎片的管理压力也会降低。
4、mcache 的存在使得内存分配不需要加锁。
当然这不是没有代价的,Go 需要预申请大块内存,这必然会出现一定的浪费,不过好在现在内存比较廉价,不用太在意。
总体上来看,Go 内存管理也是一个金字塔结构:
将有限的计算资源布局成金字塔结构,再将数据从热到冷分为几个层级,放置在金字塔结构上。调度器不断做调整,将热数据放在金字塔顶层,冷数据放在金字塔底层。这种设计利用了计算的局部性特征,认为冷热数据的交替是缓慢的。所以最怕的就是,数据访问出现冷热骤变。在操作系统上我们称这种现象为内存颠簸,系统架构上通常被说成是缓存穿透。其实都是一个意思,就是过度的使用了金字塔低端的资源。