Golang 的内存管理本质上就是一个内存池;
分配
内存池 mheap
Golang在启动的时候会像内存申请一大块内存作为内存池,这块内存池池会放在一个叫mheap的结构体中管理;mheap负责将这一整块内存切割成不同的区域,并将其中一部分的内存切割成合适的大小,分配给使用;
我们需要先知道几个重要的概念:
- page:内存页,8k大小,Go与操作系统之间的内存申请和释放都是以page为单位的;
- span:内存块,一个或多个连续的page组成一个span;
- sizeclass:空间规格;每个span都带有一个sizeclass;标记该span中的page是如何使用的;
- object:对象,用来存储一个变量数据内存空间;在一个span初始化时,他会被切割成一大堆等大的object;如一个span正好是一页大小8k,一个obect是16B,那么就可以被切割为512个object;所谓内存分配,就是分配一个obeject出去;
- mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。
- mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。
- mheap.arena_start : 将要分配给应用程序使用的空间。
mcentral
用途相同的 span 会以链表的形式组织在一起。span 被放在mcentral 的结构中管理。他有两个链表,分别是空和非空的块链表,分别表示可分配和不可分配的mspn;一个mcentral负责管理一种规格的mspn;而mcental又归mheap管理;
mcache
每一个 mcache 和每一个处理器(P) 是一一对应的,也就是说每一个 P 都有一个 mcache 成员。 在p中都会有属于该p的mcache数组,该数组的大小就表示有多少种规格的mspan,以规格sizeclass为下标;当需要分配内存的时候就会自己算出所申请的内存大小是属于那种规格的,然后以sizeclass为规格找到合适的mspn,在从mspn中分配一个object;如果mspn中没有object可分配了,就会想管理该规格mspn的mcentral获取,mcentral也没有的话就会想mheap申请,mheap负责申请page来切割成需要的mspan分配的mcentral;
Tiny对象
上面提到的 sizeclass=1 的 span,用来给 <= 8B 的对象使用,所以像 int32 , byte , bool 以及小字符串等常用的微小对象,都会使用 sizeclass=1 的 span,但分配给他们 8B 的空间,大部分是用不上的。并且这些类型使用频率非常高,就会导致出现大量的内部碎片。 所以 Go 尽量不使用 sizeclass=1 的 span, 而是将 < 16B 的对象为统一视为 tiny 对象(tinysize)。分配时,从 sizeclass=2 的 span 中获取一个 16B 的 object 用以分配。如果存储的对象小于 16B ,这个空间会被暂时保存起来 (mcache.tiny 字段),下次分配时会复用这个空间,直到这个 object 用完为止。
大对象
如上面所述,最大的 sizeclass 最大只能存放 32K 的对象。如果一次性申请超过 32K 的内存,系统会直接绕过 mcache 和 mcentral,直接从 mheap 上获取,mheap 中有一个 freelarge 字段管理着超大 span。
规格列表
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
回收
标记
bitmap
如图所示,通过gcmarkBits位图标记span的块是否被引用。对应内存分配中的bitmap区。
源码
// 计算出标记位图中有被引用的对象个数
nalloc := uint16(s.countAlloc())
if spc.sizeclass() == 0 && nalloc == 0 {
s.needzero = 1
freeToHeap = true
}
//和原来的分配位图相减得出需要被回收的对象个数
nfreed := s.allocCount - nalloc
if nalloc > s.allocCount {
print("runtime: nelems=", s.nelems, " nalloc=", nalloc, " previous allocCount=", s.allocCount, " nfreed=", nfreed, "\n")
throw("sweep increased allocation count")
}
......
if nfreed > 0 && spc.sizeclass() != 0 {
c.local_nsmallfree[spc.sizeclass()] += uintptr(nfreed)
res = mheap_.central[spc].mcentral.freeSpan(s, preserve, wasempty)
// MCentral_FreeSpan updates sweepgen
} else if freeToHeap {
.......
总结
内存的释放过程,没什么特别之处。就是分配的反过程,当 mcache 中存在较多空闲 span 时,会归还给 mcentral;而 mcentral 中存在较多空闲 span 时,会归还给 mheap;mheap 再归还给操作系统。这里就不详细介绍了。 总结一下,这种设计之所以快,主要有以下几个优势:
- 内存分配大多时候都是在用户态完成的,不需要频繁进入内核态。
- 每个 P 都有独立的 span cache,多个 CPU 不会并发读写同一块内存,进而减少 CPU L1 cache 的 cacheline 出现 dirty 情况,增大 cpu cache 命中率。
- 内存碎片的问题,Go 是自己在用户态管理的,在 OS 层面看是没有碎片的,使得操作系统层面对碎片的管理压力也会降低。
- mcache 的存在使得内存分配不需要加锁。