Go 内存管理
这里的内存管理" title="内存管理">内存管理一般指的是堆内存管理,因为栈上的内存分配和回收非常简单,不需要程序操心,而堆内存需要程序自己组织、分配和回收,用于动态分配内存。Golang内存管理的主要思想源自Google 的 TCMalloc
算法,全称 Thread-Caching Malloc
,核心思想就是把内存分为多级管理,从而降低锁的粒度。即为每个线程预分配一块缓存(Thread-cache),线程申请小内存时,可以从缓存分配内存,这样做有两个好处:
- 不必每次申请内存时都向操作系统申请,避免了系统调用,提升速度
- 由于这块缓存(Thread-cache)是每个线程独有的,因此不存在多个线程竞争的问题,多个线程同时申请小内存时,从各自的缓存分配,无需加锁,进一步提升速度
TCMalloc
算法这里就跳过了,直接介绍 Go 的内存管理,两者比较相似。
首先介绍一下 Go 内存管理的基本概念和数据结构:
Page
操作系统对内存管理以页为单位,不过这里的页不是操作系统中的页,它一般是操作系统页大小的几倍,x64 下 Page 大小是 8KB
mspan
Go中内存管理的基本单元,一组连续的Page组成1个Span。mspan
这个数据结构主要包含以下信息:
- 链表中上一个和下一个
mspan
的地址(简单说,mspan
是一个双向链表) - 起始地址(这组page的起始地址)
spanClass
,一个0~numSpanClasses
(常量,134)之间的值,可以理解为对这个mspan
的分类,或者叫它的规格,之后详细介绍,简单说就是不同spanClass的mspan
可以存储的对象大小是不一样的
mcache
与 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
链表选择空闲内存块。
mcentral
为所有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
的流程:
- 获取 加锁;从
nonempty
链表找到一个可用的mspan
;并将其从nonempty
链表删除;将取出的mspan
加入到empty
链表;将mspan
返回给工作线程;解锁。 - 归还 加锁;将
mspan
从empty
链表删除;将mspan
加入到`nonempty链表;解锁。
mheap
是堆内存的抽象,把从OS申请出的内存页组织成mspan
,并保存起来。当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。同样需要加锁访问
mheap
里保存了2棵二叉排序树(见第一张大图),按mspan
的page数量进行排序:
free
:free
中保存的mspan
是空闲并且非垃圾回收的mspan
。scav
:scav
中保存的是空闲并且已经垃圾回收的mspan
。
如果是垃圾回收导致的 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:
- 计算sizeClass,因为得到了sizeClass我们才能计算出spanClass。在Golang里sizeClass一共有67种,可以理解为对内存大小的一个分类,不同sizeClass可以保存的大小是不一样的。每个sizeClass可以保存的大小是用一个数组写死在了源码里(空间换时间):
const _NumSizeClasses = 67var 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
- 根据sizeClass计算spanClass
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
。
推荐阅读
推荐两篇不错的文章,结合着看,更加清晰:
- Go 内存分配那些事,就这么简单!
- 图解 Go 语言内存分配
以上是 Go 内存管理 的全部内容, 来源链接: utcz.com/z/264409.html