这里的内存管理一般指的是堆内存管理,因为栈上的内存分配和回收非常简单,不需要程序操心,而堆内存需要程序自己组织、分配和回收,用于动态分配内存。Golang内存管理的主要思想源自Google 的 TCMalloc 算法,全称 Thread-Caching Malloc,核心思想就是把内存分为多级管理,从而降低锁的粒度。即为每个线程预分配一块缓存(Thread-cache),线程申请小内存时,可以从缓存分配内存,这样做有两个好处:
TCMalloc算法这里就跳过了,直接介绍 Go 的内存管理,两者比较相似。
首先介绍一下 Go 内存管理的基本概念和数据结构:
操作系统对内存管理以页为单位,不过这里的页不是操作系统中的页,它一般是操作系统页大小的几倍,x64 下 Page 大小是 8KB
Go中内存管理的基本单元,一组连续的Page组成1个Span。mspan这个数据结构主要包含以下信息:
与 Thread-Cache 类似,每个线程绑定一个 mcache(具体来说是每个P绑定一个 mcache)。这样小对象直接从 mcache 分配内存,不用加锁
mcache 这个数据结构中保存了各种 spanClass 的 mspan:
- type mcache struct {
- alloc [numSpanClasses]*mspan // numSpanClasses = = _NumSizeClasses << 1,即2*67 = 134
- }
-
mcache中的alloc是一个大小为134的数组,其中的每个元素都是一个mspan双向链表,并且同一个链表上的内存块大小是相同的,相当于按照spanClass给不同规格的mspan分类存储在数组中进行管理(可以参考上面的图),这样可以根据申请的内存大小,快速从合适的mspan链表选择空闲内存块。
为所有mcache提供按照spanClass分好类的mspan资源(实际代码中每1个spanClass对应1个mcentral),当某个mcache的某个spanClass的mspan中的内存被分配光时,它会向mcentral申请一个对应spanClass的mspan。当mcache内存块多时,可以放回mcentral。mcentral被所有工作线程共享,因此需要加锁访问(获取和归还)
- // 保留重要成员变量
- type mcentral struct {
- // 互斥锁
- lock mutex
-
- // 规格
- sizeclass int32
-
- // 尚有空闲object的mspan链表
- nonempty mSpanList
-
- // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
- empty mSpanList
-
- // 已累计分配的对象个数
- nmalloc uint64
- }
-
mcache从mcentral获取和归还mspan的流程:
是堆内存的抽象,把从OS申请出的内存页组织成mspan,并保存起来。当mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。同样需要加锁访问
mheap里保存了2棵二叉排序树(见第一张大图),按mspan的page数量进行排序:
如果是垃圾回收导致的 mspan 释放,mspan 会被加入到 scav,否则加入到 free,比如刚从OS申请的的内存也组成的 mspan。
堆区总览:
主要关注图里的spans和arena区域,spans区域存放mspan的指针,而arena区域就是实际分配内存的地方,被分割成以页为单位,再把页组合起来成为Go的内存管理的基本单元mspan,mspan数据结构里面存放的起始地址信息就是指向的arena区域。bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。
当为一个对象分配内存时,Golang首先根据申请的内存大小将对象进行分类:
Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象,使用mcache的tiny分配器直接分配;
而超过32KB的大对象直接从mheap上分配,与mcentral向mheap申请内存的流程大致相同;
下面主要介绍小对象的内存分配流程。
之前说过,mspan是Golang内存管理的基本单元,所以当小对象申请内存时,Golang需要做的就是:从mcache中寻找合适的mspan进行分配;而mcache中保存的mspan双向链表又是以spanClass进行分类的(mcache中的alloc数组),所以第一步就是计算出对象申请的内存大小对应的spanClass:
- const _NumSizeClasses = 67
-
- var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
-
举个例子,如果一个对象大小在(0, 8]byte之间,对应的sizeClass就是1(往右取数组下标),对象大小在(8, 16]byte之间,对应的sizeClass就是2
- numSpanClasses = _NumSizeClasses << 1 // 2 * 67 = 134
-
可以发现sizeClass一共是67,而这里spanClass是_NumSizeClasses的两倍,原因在于为了加速之后内存回收的速度,mspan也是做了区分的,在mcache中的alloc数组里保存的mspan,有一半分配的对象不包含指针,另一半则包含指针,对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。
sizeClass到spanClass的计算如下:
- // noscan为true代表对象不包含指针
- func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
- return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
- }
-
得到spanClass之后,就可以从mcache中选择相应的mspan进行分配;如果mcache中没有相应规格的mspan,则会向mcentral申请;如果mcentral没有合适的mspan(nonempty和empty链表里都没有合适的mspan),则会向mheap申请;如果mheap没有,则会向操作系统申请。
mcentral向mheap申请时,mheap优先从free中搜索可用的mspan,如果没有找到,会从scav中搜索可用的mspan,如果还没有找到,它会向OS申请内存,再重新搜索2棵树,必然能找到mspan。如果找到的mspan比需求的大,则将其分割成2个mspan,其中1个刚好是需求大小,把剩下的再加入到free中去,然后设置需求mspan的基本信息,然后交给mcentral。