引言

Go的内存分配器基于TCMalloc(Thread Local Malloc),并在TCMalloc基础上增加了自动回收机制(TCMalloc只是增加内存申请速度,用户需手动释放空间)。

内存管理中的重要组件

在开始介绍之前,主要介绍以下Go内存分配中几个重要的组件:

  1. mheap,管理整个Go程序使用的堆空间,当程序内存不够需要申请更多内存时就是由mheap向os申请资源。操作单位是页,页大小是8KB
  2. mcentral,管理协程需要使用的内存,mcentral具有不同的规格属性,每个mcentral都管理着自身规格的内存。管理单元是mspan,mspan的规格(每个指定规格的mspan其上只能存放指定规格大小的数据)详情见源码(go/src/runtime/sizeclasses.go)。
  3. mcache,协程模型中P上的缓存内存。主要供协程使用,由于P上每次最多只可能有一个G在running,因此对P上的缓存操作无需加锁,增加了内存申请与释放的速度。缓存单位是mspan。

三者协作: mcache是P私有且无锁的内存,当协程需要内存数据时,直接从mcache上找到指定规格的mspan,并将数据存入mspan。当mspan为空(初始为空)或已满时,向mcentral申请该规格的mspan,mcentral根据自身情况,有空闲则返回,无空间则从mheap中申请,mheap则根据自身是否由空间来决定返回自身空闲空间或者向OS申请资源。

mheap

主要成员:

  1. arenas [][]heapArena
  2. allspans []mspan
  3. central []mcentral

arenas

类型为heapArena的二元数组,主要数据结构为:

1
2
3
4
5
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
}
  1. spans成员变量保存了该heapArena中的mspan,该变量长度为 heapArena大小/areaPageSize
    某个page未被mspan使用时,它的值为nil,被使用时,则该page对应的索引值即为被使用的mspan的指针(当一个mspan使用多个page时,多个page索引对应值均为该mspan指针)。
    以linux 64位为例,areaPageSize是8KB,arenaSize是64MB、所以该变量长度为 64MB/8KB=8K=8096
  2. pageInUse成员变量标记了该arena中哪些page被使用,哪些未被使用,使用位图标记,所以数组长度=pagesPerArena/8=heapArena大小/areaPageSize/8
    以linux 64位为例,areaPageSize是8KB,arenaSize是64MB、所以该变量长度为 64MB/8KB/8=1K=1024
  3. bitmap位图标记单位是words(arena中所有的words),又因为bitmap中每2个bit表示一个words的2个属性(pointer/scalar),所以bitmap数组长度应是 heapArenaBytes/(len(words)*8/2)arena字节数/words字节数/8*2
    以linux 64位为例,arenaSize是64MB,则bitmap长度是 64MB/8B/8 * 2=2M 即 2 * 1024 * 1024

allspans

顾名思义,存放着所有的mspan

central

heap.central变量是一个数组,数组长度136(0/1表示大对象),表示67种规格的mcentral对象(数组一半表示带指针,一半表示不带指针,区分主要是为了gc方便)

mcentral

1
2
3
4
5
6
7
8
9
10
type mcentral struct{
spanclass spanClass
partial [2]spanSet // list of spans with a free object 2个spanSet,一个是未sweep过,一个是sweep过的,下同
full [2]spanSet // list of spans with no free objects
}
// mcentral main func:
// cacheSpan()*mspan
// uncacheSpan(s *mspan)
// grow() *mspan
// 其他sweep相关

mcentral成员变量

  1. spanclass 表明了该mcentral对象的规格
  2. partial 表示仍有空间可使用的mspan对象
  3. full 表示无空间可用的mspan对象

mcentral核心方法

  1. cacheSpan(): 分配一个mspan对象给mcache(P上管理mspan的对象)用。
    分配过程找可用的mspan顺序:
    ① 从partial变量中sweep过的列表里找到一个有空闲的,如果有使用该mspan如果无则继续。
    ② 从partial变量中未sweep过的列表里找到一个mspan并清理它,如果有使用该mspan如果无则继续。
    ③ 从full变量中未sweep过的列表中找到一个mspan并清理它,清理后可用则使用它,如果无则继续。
    ④ 以上三个过程都未找到可用mspan则调用mcentral.grow()方法,申请空间
  2. uncacheSpan(): 从mcache中拿走一个mspan,根据其gc阶段放到对应的spanSet
  3. grow(): 从heap分配一个新的空的mspan并按照mcentral.spanclass规格将其初始化。核心调用链-> mheap_.alloc -> mheap_.allocSpan

mcache

小对象线程(GMP模型中的P)私有的的cache,因为是线程独享的所以访问时无需加锁。
mcache对象自身不在堆上(资源未托管给go的GC做清理),因此该对象中涉及到堆上的指针时需要特殊处理。

mcache主要成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type mcache struct {
// 微小对象分配使用
tiny uintptr // 指向当前微小对象可用的块(如果无,则为nil),它指向的是堆上内存地址,所以在p标记为终结时,会在 releaseAll 中处理它
// 微小对象在mspan中单个块的偏移量
// 不同规格的mspan单个块大小不一样,在linux 64位os下,微小对象使用的规格class=2,单个块大小是16byte
// 当给微小对象分配空间时,如果是需要 2byte,如果当前块中能够容下,则offset+=2,如果不能,tiny指针会找到并指向下一个块
tinyoffset uintptr
tinyAllocs uintptr // 计数
// mcache中所有规格的mspan
// 规格数是 67,但是区分了是否带指针
// 另外0、1表示大对象,所以数组实际长度是2+67*2=136
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
stackcache [_NumStackOrders]stackfreelist
// 表示上次刷新次mcache时的sweepGen
// 如果 flushGen!=mheap_.sweepgen,说明该mcache中的span是过时的,需要刷新span才能进行扫描。
// 这个过程是在acquirep中完成的
flushGen uint32
}

mcache中主要成员方法

  1. nextFree(spc spanclass) 从当前mcache中找到规格为spc的有空闲的mspan,如果没有返回空(上层会调用refill方法)
  2. refill(spc spanclass) 将当前mcache中规格为spcmspan放到对应mcentral中,并从中拿到一个可用的mspanmcache
  3. allocLarge() 大对象分配时调用,直接通过mheap_.alloc从堆中分配
  4. releaseAll() 释放所有mspan对象给mheap.central,主要是在P终结时调用

内存管理中的重要对象

  1. mheap_,mheap的实例对象。
  2. mheap_.mcentral[i] ,是具体mcentral的一个实例,mheap_.mcentral长度为136(67种规格×2是否带指针+2表示大对象)。mheap_.mcentral[i]里负责存放全局的i规格内存和管理他们的申请与释放。
  3. p.mcache[i],P上缓存内存示例,其也是一个数组,长度也为134。当p.mcache[i]为空或满时,需要申请内存就向mheap_.mcentral[i]申请。
  4. mspan,mcache、mcentral管理的内存单元。每次申请或者清理都是以mspan大小的单位,mspan具有规格属性,不同规格的mspan其大小及存放该规格数据的数量也都不尽相同。
  5. mheap_.arenas[i][j], 数据类型为heapArena,该对象主要属性:bitmap[],spans[],pageInUse,pageMarks,zeroedBase,网上很多讲go内存管理都会提到,go内存分为3部分,spans,bitmap,arena其中,arena指的就是mheap_.arenas中描述的所有内存大小,bitmap指的便是这里的属性bitmap[],spans指的便是这里的spans[],bitmap[]是字节数组,每个字节有8bit,可以用来表示4字节的内存使用情况以及gc标志,spans[]是指针数组,每个指针大小为8byte,指向一个mspan。关于说堆最大512G的说法是错误的,在linux 的go1.14中,arenas[i][j].bitmap[]大小是2MB,2MB一共是2 * 1024 * 1024 * 8 bit可以表示 8 * 1024 * 1024个地址位信息即 8M * 8byte = 64MB信息,arena数组大小是 1 * 4M,所以理论上,arenas堆空间最大内存为64MB * 4M = 256TB,bitmap占空间大小为 2MB * 4M = 8TB;spans占内存大小为 8K(数组长度) * 8B(数组指针元素8B) = 64KB(spans可表示内存:8K * 8KB(页大小)=64MB,64MB * 4M(arenas数组大小)=256TB 这与通过bitmap计算出的结果是一致的)

架构简图

注:该图基于Go1.14画的,本文更新后很多内容是基于新版本Go1.16的。其中mcentral成员变量empty noempty对应到新版本是:partial full
go内存管理

堆对象空间分配

堆对象分配的主要逻辑在 runtime/malloc.go中的 mallocgc() 函数

微小(tiny)对象分配 (0,16byte]

按照代码中的逻辑,当待分配空间的对象大小<=16byte且没有指针引用时,使用微小对象分配的方法,将其放到mcache.alloc[5]的mspan中,这个mspan的规格是16byte。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Tiny allocator.
//
// Tiny allocator combines several tiny allocation requests
// into a single memory block. The resulting memory block
// is freed when all subobjects are unreachable. The subobjects
// must be noscan (don't have pointers), this ensures that
// the amount of potentially wasted memory is bounded.
//
// Size of the memory block used for combining (maxTinySize) is tunable.
// Current setting is 16 bytes, which relates to 2x worst case memory
// wastage (when all but one subobjects are unreachable).
// 8 bytes would result in no wastage at all, but provides less
// opportunities for combining.
// 32 bytes provides more opportunities for combining,
// but can lead to 4x worst case wastage.
// The best case winning is 8x regardless of block size.
//
// Objects obtained from tiny allocator must not be freed explicitly.
// So when an object will be freed explicitly, we ensure that
// its size >= maxTinySize.
//
// SetFinalizer has a special case for objects potentially coming
// from tiny allocator, it such case it allows to set finalizers
// for an inner byte of a memory block.
//
// The main targets of tiny allocator are small strings and
// standalone escaping variables. On a json benchmark
// the allocator reduces number of allocations by ~12% and
// reduces heap size by ~20%.
off := c.tinyoffset
// 将微对象对齐
if size&7 == 0 {
off = alignUp(off, 8)
} else if size&3 == 0 {
off = alignUp(off, 4)
} else if size&1 == 0 {
off = alignUp(off, 2)
}
if off+size <= maxTinySize && c.tiny != 0 {
// 如果能被当前的内存块放下,则放到当前内存块
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.local_tinyallocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 否则,取出微对象指定规格的mspan,从mspan中获取下个能够存放微对象的内存块
span := c.alloc[tinySpanClass]
v := nextFreeFast(span)
// 如果没找到,那么执行nextFree,从下层(mcentral)申请一个mspan,c.nextFree -> c.refill -> mheap_.central[spc].mcentral.cacheSpan
if v == 0 {
v, _, shouldhelpgc = c.nextFree(tinySpanClass)
}
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
// 后续就是处理新申请的资源,同时修改当前mcache中关于其的元数据信息
// See if we need to replace the existing tiny block with the new one
// based on amount of remaining free space.
if size < c.tinyoffset || c.tiny == 0 {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize

小对象分配 (16byte,32kb)

小对象内存分配,比微对象少了从 mcache.tiny上快速分配空间的逻辑,同微对象后边的分配空间逻辑大致相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 确定sizeclass
var sizeclass uint8
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
} else {
sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
}
size = uintptr(class_to_size[sizeclass])
spc := makeSpanClass(sizeclass, noscan)
span := c.alloc[spc]
// 从当前mspan找空闲内存块,找不到,则执行nextFree向底层申请
v := nextFreeFast(span)
if v == 0 {
v, span, shouldhelpgc = c.nextFree(spc)
}
x = unsafe.Pointer(v)
// 根据需求,是否清理分配出来的内存为0
if needzero && span.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(v), size)
}

大对象分配 (32kb,∞)

大对象内存分配直接从heap申请资源

1
2
3
4
5
6
7
8
9
var s *mspan
shouldhelpgc = true
systemstack(func() {
s = largeAlloc(size, needzero, noscan)
})
s.freeindex = 1
s.allocCount = 1
x = unsafe.Pointer(s.base())
size = s.elemsize