Go 内存分配

Go memory

Posted by cslqm on December 16, 2020

内置运⾏时的编程语⾔通常会抛弃传统的内存分配⽅式,改由⾃主管理。这样可以完成类似预分配、内存池等操作,以避开系统调⽤带来的性能问题。当然,还有⼀个重要原因是为了更好地配合垃圾回收。

内存基本策略

  1. 每次从操作系统申请⼀⼤块内存(⽐如 1MB),以减少系统调⽤。
  2. 将申请到的⼤块内存按照特定⼤⼩预先切分成⼩块,构成链表。
  3. 为对象分配内存时,只需从⼤⼩合适的链表提取⼀个⼩块即可。
  4. 回收对象内存时,将该⼩块内存重新归还到原链表,以便复⽤。
  5. 如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。

内存分配器只管理内存块,并不关⼼对象状态。且不会主动回收内存,由垃圾回收器在完成清理操作后,触发内存分配器回收操作。

内存块

分配器将其管理的内存块分为两种:

  • span:由多个地址连续的页组成的大块内存。
  • object:将 span 按照特定大小切割成多个小块,每个小块可存储一个对象。

分配器按照页数来区分不同大小的 span。比如,以页数为单位将 span 存放到管理数组中,需要时就以页数为索引进行查找。当然,span 大小并非固定不变。分配器还会尝试将地址相邻的空闲 span 合并。

malloc.go

1
2
_PageShift = 13
_PageSize  = 1 << _PageShift
1
2
3
4
5
6
7
type mspan struct {
        next     *mspan    // in a span linked list
        prev     *mspan    // in a span linked list
        start    pageID    // starting page number
        npages   uintptr   // number of pages in span
	freelist gclinkptr // list of free objects
}

⽤于存储对象的 object,按 8 字节倍数分为 n 种。⽐如说,⼤⼩为 24 的 object 可⽤来存储范围在 17 ~ 24 字节的对象。这种⽅式虽然会造成⼀些内存浪费,但分配器只需⾯对有限的⼏种规格(size class)⼩块内存,优化了分配和复⽤管理策略。

分配器会尝试将多个微⼩对象组合到⼀个 object 块内,以节约内存。

分配器初始化时,会构建对照表存储⼤⼩和规格的对应关系,包括⽤来切分的 span 页数。

msize.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Size classes.  Computed and initialized by InitSizes.
//
// SizeToClass(0 <= n <= MaxSmallSize) returns the size class,
//      1 <= sizeclass < NumSizeClasses, for n.
//      Size class 0 is reserved to mean "not small".
//
// class_to_size[i] = largest size in class i
// class_to_allocnpages[i] = number of pages to allocate when
//      making new objects in class i

var class_to_size [_NumSizeClasses]int32
var class_to_allocnpages [_NumSizeClasses]int32

var size_to_class8 [1024/8 + 1]int8
var size_to_class128 [(_MaxSmallSize-1024)/128 + 1]int8

若对象⼤⼩超出特定阈值限制,会被当做⼤对象(large object)特别对待。

malloc.go

1
2
        // Tunable constants.
        _MaxSmallSize = 32 << 10   // 32KB

管理组件

1.5.1 中,分配器由三种组件构成。

  • cache:每个运行期工作线程都会绑定一个 cache,用于无锁 object 分配。
  • central:为所有 cache 提供切分好的后备 span 资源。
  • heap:管理闲置 span,必要时向操作系统申请内存。

mheap.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type mheap struct {
        free      [_MaxMHeapList]mspan // 闲置 span 链表数组,页数在 127 内的
	freelarge mspan                // 闲置 span 链表数组,页数 >= 127
        busy      [_MaxMHeapList]mspan // busy lists of large objects of given length
        busylarge mspan                // busy lists of large objects length >= _MaxMHeapList

        // central free lists for small size classes.
        // the padding makes sure that the MCentrals are
        // spaced CacheLineSize bytes apart, so that each MCentral.lock
	// gets its own cache line.
	// 每个 central 对应一种 sizeclass
        central [_NumSizeClasses]struct {
                mcentral mcentral
                pad      [_CacheLineSize]byte
        }
}

mcentral.go

1
2
3
4
5
6
7
// Central list of free objects of a given size.
type mcentral struct {
        lock      mutex
        sizeclass int32
        nonempty  mspan // list of spans with a free object
        empty     mspan // list of spans with no free objects (or cached in an mcache)
}

mcache.go

1
2
3
4
5
6
7
// Per-thread (in Go, per-P) cache for small objects.
// No locking needed because it is per-thread (per-P).
type mcache struct {
        // The rest is not accessed on every malloc.
	alloc [_NumSizeClasses]*mspan // spans to allocate from
	
}