# 16. 内存模型
总结
内存结构:
- Go 将堆内存抽象为 mheap 结构体;
- Go 进程会从虚拟内存中申请 n 个 heapArena;
- 每个 heapArena 被按需划分成不同 class 的 mspan,class 有 68 个等级;
- 每个 mspan 由 n 个相同大小的 span 组成;
- 为了快速寻找到对应 span,为 mheap 建立了 136 个中央索引 mcentral;
- 每个 mcentral 存储每种 class 的 mspan,每种 mspan 又划分为 gc scan 和 no scan 两种,故共有 68 × 2 = 136 个 mcentral;
- 为了解决中央索引在并发性的锁竞争问题,为每一个 p(thread)建立一个本地缓存 mcache;
- 每个 mcache 存储 128 个 span,分别是每种 class 的 mspan 的一个 scan 和 noscan 的 span;
内存分配:
- Go 中将根据对象大小分为 tiny、small 和 large 三种对象;
- tiny 和 small 对象会被分配到 class1 ~ class67 的 span 中;
- large 对象会量身定做分配到 class0 的 span 中,直接从 mheap 上申请;
- 为对象分配内存时,会先从 mcache 上找 span,找不到就去 mcentral 上交换,还找不到,就去 mheap 上申请,还找不到,就 OOM。
# 16.1 协程栈
# 16.1.1 作用
- 记录协程的执行路径
- 记录局部变量
- 函数传参
- 函数返回值
# 16.1.2 位置
- Go 协程栈位于 Go 堆内存上
- Go 堆内存位于操作系统虚拟内存上
# 16.1.3 图解
package main
func sum(a, b int) int {
sum := 0
sum = a + b
return sum
}
func main() {
a := 3
b := 5
print(sum(a, b))
}
# 16.1.4 参数传递
- Go 是值传递
- 传递结构体时:会拷贝结构体中的全部内容
- 传递结构体指针时:会拷贝结构体指针
# 16.1.5 栈大小
默认 2KB
# 16.1.6 逃逸分析
不是所有的变量都能放在协程栈上,例如栈帧回收后需要继续使用的变量和太大的变量,都会逃逸到 堆 上。
具体有三种逃逸:
- 指针逃逸:函数返回对象的指针
- 空接口逃逸:函数参数为 interface{},那么函数的实参很可能会逃逸,因为底层很可能会用到反射
- 大变量逃逸:变量太大,栈帧放不下,就逃逸到堆上了。64 位机器中,一般超过 64KB 的变量就会逃逸
# 16.1.7 栈扩容
Go 栈的初始空间为 2KB。在函数调用前会执行 morestack
判断栈空间是否足够,如果不够,就需要对栈进行扩容。
- 分段栈:Go1.13 之前
- 没有空间浪费,但是栈帧会在不连续的空间之间横跳,性能较差。
- 连续栈:Go1.13 及之后
- 开辟一整块新的大栈,然后把老的拷贝过来,保证了空间一直连续。但是伸缩时的开销较大。
# 16.2 虚拟内存单元 heapArena
# 16.2.1 概述
- 物理内存为 64G 的机器中,每个 Go 进程可以会被分配到 256TB 的虚拟内存。
- Go 的虚拟内存单元为
heapArena
,每次申请 64M。 - 最多可以申请 220 个。
- 所有的
heapArena
组成了mheap
(Go 堆内存)
# 16.2.2 底层
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte
spans [pagesPerArena]*mspan // heapArena 会被分成很多个 mspan
pageInUse [pagesPerArena / 8]uint8 // 哪些页正在被使用中
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
zeroedBase uintptr
}
# 16.2.3 分配策略
线性分配 | 链表分配 | 分级分配 |
---|---|---|
实现简单,但内存碎片较多。 | 将空闲块连接起来,牺牲了一部分性能,来能缓解一部分的内存碎片。 | 将 heapArena 按级别分成很多个块,根据对象大小存放在能容纳它的最小的块中。 |
Go 参考了分级分配策略,将每一个级定义为 mspan ,mspan 为 N 个相同大小 span 组成 |
PS:分级分配参考了 TCMalloc (opens new window)
# 16.3 内存管理单元 mspan
# 16.3.1 概述
- Go 使用内存时的最小单位是
mspan
。 - 每个
mspan
为 N 个相同大小的span
组成。 - Go 中一个有
67
种mspan
。
// runtime/sizeclasses.go
// 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 24 8192 341 8 29.24%
// ...
// 62 20480 40960 2 0 6.87%
// 63 21760 65536 3 256 6.25%
// 64 24576 24576 1 0 11.45%
// 65 27264 81920 3 128 10.00%
// 66 28672 57344 2 0 4.91%
// 67 32768 32768 1 0 12.50%
# 16.3.2 底层
type mspan struct {
next *mspan // 下一个 span
prev *mspan // 上一个 span
...
spanclass spanClass // 级别
...
}
# 16.4 中心索引 mcentral
# 16.4.1 概述
heapArena 中的 mspan 不是一开始就全部划分好的,而且按需划分。
由于每个 heapArena 中的 mspan 都是不确定的,为了给要分配空间的对象快速定位到合适的 mspan,Go 中定义了中心索引 mcentral
。
总共有 136
个 mcentral
结构体,其中 68
个需要 gc 扫描的 mspan,68
个不需要 gc 的 mspan。
# 16.4.2 底层
type mcentral struct {
spanclass spanClass // class 级别
partial [2]spanSet // 未满的 span
full [2]spanSet // 满的 span
}
# 16.5 线程缓存 mcache
# 16.5.1 概述
mcentral
实际是一个中心索引,修改它需要使用互斥锁进行保护,锁冲突会造成性能问题。Go 参考 GMP 模型,增加线程本地缓存。给每一个线程做了一个二级缓存 mcache
,极大缓解了并发锁争夺的性能消耗。
- 每个
P
有一个mcahce
。 - 对于每一种 class 的 mcentral,取一个 scan 和一个 noscan,这样一个
mcache
拥有136
个mspan
。其中 68 个需要 GC 扫描,68 个不需要。 - 当本地索引用完了之后,就上锁,去中央索引进行交换。
# 16.5.2 底层
type mcache struct {
nextSample uintptr
scanAlloc uintptr
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan // 136 个 msapn
stackcache [_NumStackOrders]stackfreelist
flushGen uint32
}
type p struct {
...
mcache *mcache
...
}
# 16.6 堆 mheap
最后,我们来看看 Go 中对堆内存的抽象结构:mheap
type mheap struct {
// 锁
lock mutex
// mspan 是按需分级的,这里保存了所有一个划分好的 mspan
allspans []*mspan
// mheap 被划分为多个 heapArena
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
// 中心索引
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
// 其他省略
...
}
# 16.7 内存分配
# 16.7.1 对象分级
Tiny(0 ~ 16B)
:微对象,无指针Small(16B~32KB)
:小对象Large(32KB,+∞)
:大对象
Tiny
和 Small
会被分配到普通的 mspan(class1 ~ class67)
Large
会被分配到量身定做的 mspan(class0)
# 16.7.2 微对象分配
- 从 mcache 拿到 mspan-class2;
- 将多个
tiny
合并成一个 16Bytes 存入; - 如果 mcache 中没有空闲的 span,那就去 mcentral 兑换新的 span;
- 如果 mcentral 中还没,那就去 mheap 上申请;
# 16.7.3 小对象分配
- 根据对象大小确定 mspan 的 class;
- 在 mcache 中寻找对应 class 的 span 进行分配;
- 如果 mcache 中没有空闲的 span,那就去 mcentral 兑换新的 span;
- 如果 mcentral 中还没,那就去 mheap 上申请;
# 16.7.4 大对象分配
量身定做 class0,直接从 mheap 上申请内存。
// allocLarge allocates a span for a large object.
func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {
// 1. 检查 OOM,对象太大了顶不住
if size+_PageSize < size {
throw("out of memory")
}
// 2. 检查大对象需要多少 page
npages := size >> _PageShift
if size&_PageMask != 0 {
npages++
}
// 3. 大对象统一分配到 class0
spc := makeSpanClass(0, noscan)
// 4. 直接从 mheap 中申请 class0 的 span
s := mheap_.alloc(npages, spc, needzero)
...
// 5. 申请完了之后,再将该 span 的索引建立到 mcentral 上,方便 gc 扫描
mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
s.limit = s.base() + size
heapBitsForAddr(s.base()).initSpan(s)
return s
}
# 16.7.5 mcache 替换
- 在 mcache 中,每个 class 的 mspan 只有一个,当 mspan 满了之后,会从 mcentral 中兑换一个新的。
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
shouldhelpgc = false
freeIndex := s.nextFreeIndex()
if freeIndex == s.nelems {
// mcache 中的 span 满了,要去 mcentral 中兑换一个新的
c.refill(spc)
shouldhelpgc = true
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
...
}
func (c *mcache) refill(spc spanClass) {
// 1. 将当前 mcache 中的 span 释放回 mcentral
s := c.alloc[spc]
if s != &emptymspan {
if s.sweepgen != mheap_.sweepgen+3 {
throw("bad sweepgen in refill")
}
mheap_.central[spc].mcentral.uncacheSpan(s)
}
// 2. 从 mcentral 中获得一个新的 span
s = mheap_.central[spc].mcentral.cacheSpan()
...
}
# 16.7.6 mcentral 扩容
- mcentral 中,只有有限数量的 mspan,当 mspan 缺少时,会像 mheap 中开辟新的 heapArena,并申请对应 class 的 span。
// Allocate a span to use in an mcache.
func (c *mcentral) cacheSpan() *mspan {
...
// 1. mcache 想从 mcentral 中兑换新的span,
// 发现 mcentral 已经没有可用的 span 了
// 那就只能开辟新的 heapArena,并申请对应 class 的 span 了
s = c.grow()
if s == nil {
return nil
}
...
return s
}
// grow allocates a new empty span from the heap and initializes it for c's size class.
func (c *mcentral) grow() *mspan {
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
// 在 mheap 中分配新的 heapArena 和申请对应 class 的 span
s := mheap_.alloc(npages, c.spanclass, true)
if s == nil {
return nil
}
n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
s.limit = s.base() + size*n
heapBitsForAddr(s.base()).initSpan(s)
return s
}
# 16.7.7 mallocgc 源码分析
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
var span *mspan
var x unsafe.Pointer
noscan := typ == nil || typ.ptrdata == 0
if size <= maxSmallSize {
// ① tiny 微对象
if noscan && size < maxTinySize {
// 1. 合并多个微对象,尽可能到 16B
off := c.tinyoffset
if size&7 == 0 {
off = alignUp(off, 8)
} else if sys.PtrSize == 4 && size == 12 {
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.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 2. 在 mcache 中找到分配给当前线程的 span
span = c.alloc[tinySpanClass]
// 3. 尝试获取 mcache 中的 span
v := nextFreeFast(span)
if v == 0 {
// 4. 如果 mcache 中对应 class 的 span 已经被分配了
// 就去中央索引中交换空闲的 span
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
// 5. 分配内存
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
if size < c.tinyoffset || c.tiny == 0 {
c.tiny = uintptr(x)
c.tinyoffset = size
}
size = maxTinySize
} else {
// ② small 小对象
var sizeclass uint8
// 1. 根据对象大小去查表,找到对应 class 的级别
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
size = uintptr(class_to_size[sizeclass])
// 2. 确定 mspan 的 class
spc := makeSpanClass(sizeclass, noscan)
// 3. 在 mcache 中找到分配给当前线程的 span
span = c.alloc[spc]
// 4. 尝试获取 mcache 中的 span
v := nextFreeFast(span)
if v == 0 {
// 5. 如果 mcache 中对应 class 的 span 已经被分配了
// 就去中央索引换空闲的 span 来分配
v, span, shouldhelpgc = c.nextFree(spc)
}
// 6. 分配内存
x = unsafe.Pointer(v)
if needzero && span.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(v), size)
}
}
} else {
// ③ large 大对象
shouldhelpgc = true
// 针对大对象量身定制一个 class0 的 span
span = c.allocLarge(size, needzero, noscan)
span.freeindex = 1
span.allocCount = 1
x = unsafe.Pointer(span.base())
size = span.elemsize
}
...
return x
}