go runtime抛弃了传统的内存分配方式,改为自主管理。其内存分配算法主要源自 Google为C语言开发的
TCMalloc算法
。其核心思想是把内存分为多级管理,从而降低锁的粒度
基础概念
go维护着一块大的全局内存;每个调度器(P,Processer)维护一块小的私有内存,不足时再从全局申请。
预申请的内存分为:
- spans:存放span的指针(指向对应page),共需要
(512G/8KB)*8byte=512M
(8KB为page大小,8byte为指针大小); - bitmap:主要用于GC(垃圾回收);
- arena:即堆区(共512G,按8K划分为page);
span
span是用于管理arena页的关键数据结构,是内存管理的基本单位,每个span包含一个或多个连续页:
- 为满足小对象分配,span中一页会划分为更小粒度;
- 大对象(超过页大小),则通过多页实现;
每个span用于管理特定的class对象,对象class大小及与span间关系如下图所示:
- class:每个span结构中都有一个class ID,表示此span可处理的对象类型;
- bytes/obj:每个class对应对象的字节数;
- bytes/span:每个span占用堆的字节数;
- objects:每个span可分配对象数;
- waste bytes:每个span的内存碎片(及浪费的内存);
span中最大对象是32K,超过此大小的由特殊class(class ID为0)表示。
span结构类似:
type mspan struct {
next *mspan //链表前向指针, 用于将span链接起来
prev *mspan //链表前向指针, 用于将span链接起来
startAddr uintptr // 起始地址, 也即所管理页的地址
npages uintptr // 管理的页数
nelems uintptr // class对象个数,即可分配出多少个对象
allocBits *gcBits //分配位图, 每一位代表一个块是否已分配
allocCount uint16 // 已分配块的个数
spanclass spanClass // class表中的class ID
elemsize uintptr // class表中的对象大小, 也即块大小
}
span管理内存如下所示(里面包含class标识与大小,以及分配的数量):
cache
管理span的数据结构为mcentral,各线程从mcentral管理的span中申请内存;为避免申请内存时加锁,golang为每个线程分配了span的缓存(cache)。
type mcache struct {
alloc [67*2]*mspan // 按class分组的mspan列表
}
每个class类型都有两组span列表(为提高GC扫描性能:不包含指针的span列表不需要扫描):
- 第一组:表示的对象包含了指针;
- 第二组:表示的对象不含指针;
根据对象是否包含指针,划分为noscan与scan两类
mchache在初始化时没有任何span,在使用过程中动态从central中获取并缓存。
central
central是全局资源,为多个线程服务(线程内存不足时,向central申请;当线程释放内存时,再放回central)
type mcentral struct {
lock mutex //互斥锁
spanclass spanClass // span class ID
nonempty mSpanList // non-empty 指还有空闲块的span列表
empty mSpanList // 指没有空闲块的span列表
nmalloc uint64 // 已累计分配的对象个数
}
线程从central获取span步骤:
- 加锁
- 从nonempty链表中获取可用span,并从链表中删除;
- 将取出的span放入empty链表;
- 将span返回给线程;
- 解锁;
- 线程将span缓存进cache;
heap
mheap代表go程序持有的所有堆空间,golang通过一个mheap全局变量进行内存管理:
当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。mheap
主要用于大对象的内存分配,以及管理未切割的mspan
,用于给mcentral
切割成小对象。
mheap
中含有所有规格的mcentral
,所以,当一个mcache
从mcentral
申请mspan
时,只需要在独立的mcentral
中使用锁,并不会影响申请其他规格的mspan
。
分配过程
针对不同的对象有不同的分配逻辑:
- (0,16B)且不包含指针的对象:Tiny分配;
- (0,16B)包含指针的对象:正常分配;
- [16B,32KB]:正常分配;
- (32KB, -):大对象分配。
分配size为n的对象步骤:
- 获取当前线程私有缓存mcache;
- 根据size计算出合适的classID;
- 从mcache的alloc[class]链表中查询可用span;
- 若mcache中无可用span,则从mcentral中申请一个新的span放入mcache中;
- 若mcentral中也无可用span,则从mheap中申请一个新的span加入到mcentral中;
- 从span中获取空闲对象地址并返回;