2021年这么快就来了。。。。。。。。。
Golang运行时的内存分配算法主要源自Google为C语言开发的TCMalloc算法,全称Thread-Caching Malloc。核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
Golang中也实现了内存分配器,原理与tcmalloc类似,简单的说就是维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存,私有内存不足再从全局申请。
一、概念
预申请的内存划分为 spans、bitmap、arena三个部分。arena即所谓的堆区,应用中需要的内存从这里分配。spans和bitmap是为了管理arena区而存在的。
- arena:大小为512GB,为了管理方便,把arena区域划分成一个个的页(page),每个page大小为8KB,一共有512GB/8KB个页。一些页组合起来称为mspan
- spans:存放mspan(也就是一些arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一个或多个page,所以spans区域的大小为 (512GB/8KB) X 指针大小(8byte) = 512GB。除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域所有指针的大小。创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan。
- bitmap:标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,所以bitmap区域的大小是512GB/(4*8B)=16GB。该区域主要用于GC
二、内存管理单元
根据对象的大小,划分了一组class,每个class都表示一个固定大小的对象,以及大小。
参照src/runtime/mksizeclasses.go|src/runtime/sizeclasses.go,如下所示,其中
- class: classId,每个span结构中都有一个class ID,表示该span可处理的对象类型
- bytes/obj:该class代表对象的字节数
- bytes/span:每个span占用堆得字节数,即页数 X 页大小
- objects: 每个span可分配的对象个数,即 ( byte / spans ) / ( bytes / obj )
- waste bytes: 每个span产生的内存碎片,即 ( bytes / spans) % ( bytes / obj)
最大的对象(class66)大小事32KB,超过32KB大小的对象由特殊的class表示,该classID为0,每个class值包含一个对象。
1 | class bytes/obj bytes/span objects tail waste max waste |
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。作为内存管理的基本单位,每个span管理特定的class对象。
src/runtime/mheap.go:mspan定义了span的数据结构。
1 | type mspan struct { |
三、内存管理组件
1、cache
为了避免多线程申请内存时不断的加锁,Go为每个P分配了span的缓存,这个缓存就是cache。
src/runtime/mcache.go
1 | numSpanClasses = _NumSizeClasses << 1 //67 * 2 |
alloc数组大小为class总数的2倍。数组的每个元素代表一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象包含了指针,第二组列表中所示的对象不包含指针,此举是为了提高GC的扫描性能,对于不包含指针的span列表没必要去扫描。
mcache和mspan的关系如下:
mcache在初始化的时候是没有任何mspan资源的,在使用过程中会动态地从mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache的相应规格的mspan进行分配。
2、central
central作为全局资源为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收金central。
src/runtime/mcentral.go
1 | type mcentral struct { |
获取span过程
加锁;
从nonempty链表找到一个可用的mspan;并将其从nonempty链表删除;
将取出的mspan加入到empty链表;
将mspan返回给工作线程;
解锁。
归还span过程
加锁;
将mspan从empty链表删除;
将mspan加入到nonempty链表;
解锁。
3、heap
每种class都会对应一个central,这个central的集合会防御heap中。heap代表Go程序持有的所有堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。
src/runtime/mheap.go
1 | type mheap struct { |
四、分配流程
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
- ( 0, 16B)不包含指针: tiny分配
- ( 0, 16B)包含指针: 正常分配
- [ 16B, 32KB]:正常分配
- ( 32KB, ∞):大对象分配
分配步骤如下:
1、获取当前线程的私有缓存mcache
2、根据size计算出适合的classID
3、从mcache的alloc[classID]链表中查询看可用的span
4、如果mcache没有可用的span,则从mcentral申请一个新的span加入mcache
5、如果mcentral中也没有可用的span,则从mheap中申请一个新的span加入mcentral
6、从该span中获取空闲对象地址并返回