内置运⾏时的编程语⾔通常会抛弃传统的内存分配⽅式,改由⾃主管理。这样可以完成类似预分配、内存池等操作,以避开系统调⽤带来的性能问题。当然,还有⼀个重要原因是为了更好地配合垃圾回收。
内存基本策略
- 每次从操作系统申请⼀⼤块内存(⽐如 1MB),以减少系统调⽤。
- 将申请到的⼤块内存按照特定⼤⼩预先切分成⼩块,构成链表。
- 为对象分配内存时,只需从⼤⼩合适的链表提取⼀个⼩块即可。
- 回收对象内存时,将该⼩块内存重新归还到原链表,以便复⽤。
- 如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。
内存分配器只管理内存块,并不关⼼对象状态。且不会主动回收内存,由垃圾回收器在完成清理操作后,触发内存分配器回收操作。
内存块
分配器将其管理的内存块分为两种:
- 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
}