Go

go的内存分配与回收

go学习记录

Posted by CHuiL on May 22, 2019

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出去;

image

image

  • 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。 image

image

规格列表

// 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区。 image

源码

// 计算出标记位图中有被引用的对象个数
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 {
.......
    

image

总结

内存的释放过程,没什么特别之处。就是分配的反过程,当 mcache 中存在较多空闲 span 时,会归还给 mcentral;而 mcentral 中存在较多空闲 span 时,会归还给 mheap;mheap 再归还给操作系统。这里就不详细介绍了。 总结一下,这种设计之所以快,主要有以下几个优势:

  1. 内存分配大多时候都是在用户态完成的,不需要频繁进入内核态。
  2. 每个 P 都有独立的 span cache,多个 CPU 不会并发读写同一块内存,进而减少 CPU L1 cache 的 cacheline 出现 dirty 情况,增大 cpu cache 命中率。
  3. 内存碎片的问题,Go 是自己在用户态管理的,在 OS 层面看是没有碎片的,使得操作系统层面对碎片的管理压力也会降低。
  4. mcache 的存在使得内存分配不需要加锁。