高并发下的内存模型与垃圾回收
Go内存模型
为什么说Go的栈在堆上
Go协程栈的位置
- Go协程栈位于Go堆内存上。
- Go堆内存位于操作系统虚拟内存上。
Go使用参数拷贝传递(值传递)
- 传递结构体时:会拷贝结构体中的全部内容。
- 传递结构体指针时:会拷贝结构体指针。
总结
- 协程栈记录了协程的执行现场。
- 协程栈还负责记录局部变量,传递参数和返回值。
- Go使用参数拷贝传递。
协程栈不够用了怎么办
局部变量太大导致协程栈不够用
逃逸分析
- 不是所有的变量都能放在协程栈上。
- 栈帧回收后,需要继续使用的变量。
- 太大的变量。
指针逃逸
- 函数返回了对象的指针。
空接口逃逸
- 如果函数参数为interface{}。
- 函数的实参很可能会逃逸。
- 因为interface{}类型的函数往往会使用反射。
大变量逃逸
- 过大的变量会导致栈空间不足
- 64位机器中,一般超过64KB的变量会逃逸。
栈扩容
- Go栈的初始空间为2KB。
- 在函数调用前判断栈空间(morestack)
- 必要时对栈进行扩容。
连续栈方式扩容(1.14版本开始)
- 优点:空间一直连续
- 缺点:伸缩时的开销大。
- 当空间不足时扩容,变为原来的2倍。
- 当空间使用率不足1/4时缩容,变为原谅的1/2.
Go的堆内存结构是怎样的?
heapArena
- Go每次申请的虚拟内存单元为64MB。
- 最多有4194304个虚拟内存单元(2的20次方)
- 内存单元也叫heapArena
- 所有的heapArena组成了mheap(Go堆内存)
- 线性分配或者链表分配容易出现空间碎片。
Go分配heapArena的方式——分级分配
内存管理单元mspan
- 根据隔离适应策略,使用内存时的最小单位为mspan。
- 每个mspan为N个相同大小的“格子”
- Go中一共有67种mspan。
每个heapArena中的span都不确定,那如何快速找到所需的mspan级别?
中心索引 mcentral
- 136个mcentral结构体,其中
- 68个组需要GC扫描的mspan
- 68个组不需要GC扫描的mspan
mcentral的性能问题
- mcentral实际是中心索引,使用互斥锁保护。
- 在高并发场景下,锁冲突问题严重。
- 参考GMP模型,增加线程本地缓存。
线程缓存 mcache
- 每个P拥有一个mcache。
- 一个mcache拥有136个mspan,其中
- 68个需要GC扫描的mspan
- 68个不需要GC扫描的mspan
总结
- Go模仿TCmalloc,建立了自己的堆内存架构。
- 使用heapArena向操作系统申请内存。
- 使用heapArena时,以mspan为单位,防止碎片化。
- mcentral是mspan们的中心索引(存在锁竞争问题)。
- 所以引入了mcache,mcache记录了分配给各个P的本地mspan。
Go是如何分配堆内存的
- 微小对象分配至普通mspan。
- 大对象量身定做mspan。
微对象分配
- 从mcache拿到2级mspan
- 将多个微对象合并成一个16Byte存入
mcache的分配
- mcache中,每个级别的mspan只有一个。
- 当mspan满了之后,会从mcentral中换一个新的。
mcentral的扩容
- mcentral中,只有有限数量的mspan。
- 当mspan缺少时,会从heapArena开辟新的mspan。
大对象分配
- 直接从heapArena开辟0级的mspan。
- 0级的mspan为大对象定制。
heapArena的扩充
- 当heapArena空间不足时。
- 向操作系统申请新的heapArena。
总结
- Go将对象按照大小分为3种。
- tiny对象和small对象使用mcache。
- mcache中的mspan填满后,与mcentral交换新的。
- mcentral不足时,在heapArena开辟新的mspan。
- 大对象之间在heapArena开辟新的mspan。
什么样的对象需要垃圾回收
垃圾回收(Garbage Collection)思路
- Go因为堆内存结构的独特优势,选择最简单的标记-清除算法。
- 找到有引用的对象,剩下的就是没有引用的。
那么就引出了下一个问题:
从哪开始找引用的对象,即GC的起点?
- 被栈上的指针引用
- 被全局变量指针引用
- 被寄存器中的指针引用
- 上述变量被称为Root Set(GC Root)
- 这种方法也叫可达性分析标记法
并发垃圾回收
- 并发的难点在于标记阶段
三色标记法
- 黑色:黑色对象本身是有用的,且该对象指向的对象已经被分析扫描
- 灰色:灰色对象本身是有用的,但是该对象指向的对象还未被分析扫描。
- 白色:还未扫描到的对象或者被扫描过确认是无用的对象。
三色标记法是为了解决并发标记中存在的问题
并发标记问题一:删除
yuasa删除屏障
- 并发标记时
- 对指针释放的白色对象置灰
并发标记问题二:插入
Dijkstra插入屏障
- 并发标记时
- 对指针新指向的白色对象置灰。
Go语言使用的是混合屏障
- 被释放引用的堆对象标记为灰色
- 被添加引用的堆对象标记为灰色
如何优化GC效率
GC触发的时机
- 系统定时触发
- 用户显式触发
- 申请内存时触发
系统定时触发
- sysmon定时检查
- 如果2分钟内没有过GC,触发。
- 谨慎调整
用户显式触发
- 用户调用runtime.GC 方法
- 并不推荐调用
申请内存时触发
- 给对象申请堆空间时,可能导致GC。
GC优化原则
- 尽量少在堆上产生垃圾
- 内存池化
- 减少逃逸
- 使用空结构体
内存池化
- 有缓存性质的对象
- 频繁创建和删除
- 使用内存池,不GC。
减少逃逸
- 逃逸会使原本在栈上的对象进入堆中。
- 尽量少用fmt包,使用log包等
- 返回了指针而不是拷贝,留心指针逃逸问题。
使用空结构体
- 空结构体指向一个固定地址
- 不占用堆空间
- 比如Channel传递空结构体
GC分析工具
- go tool pprof
- go tool race
- go build -gcflags=" -m"
- GODEBUG=" gctrace=1"
/*
使用 $env:GODEBUG="gctrace=1" 查看gc情况
*/
func main() {
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(wg *sync.WaitGroup) {
var count int
for i := 0; i < 1e10; i++ {
count++
}
wg.Done()
}(&wg)
}
}
$env:GODEBUG="gctrace=1"
gc 1 @0.115s 0%: 0+1.6+0 ms clock, 0+0.55/2.9/0+0 ms cpu, 3->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
practice on main ≡ ~1 | +1
➜ go run .\9.9.go
gc 1 @0.089s 0%: 0+1.0+0 ms clock, 0+0/0/1.0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 2 @0.108s 0%: 0+0.63+0 ms clock, 0+0.63/0.63/0.63+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 3 @0.140s 0%: 0+0.54+0 ms clock, 0+0/1.0/0.54+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 4 @0.188s 0%: 0+0.52+0 ms clock, 0+0.52/0.52/0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 5 @0.264s 0%: 0+0.55+0.53 ms clock, 0+0/1.6/1.1+8.5 ms cpu, 4->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 6 @0.273s 0%: 0+0.63+0 ms clock, 0+0/0.32/1.0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 7 @0.396s 0%: 0.15+2.1+0 ms clock, 2.5+1.1/0/1.6+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
# command-line-arguments
gc 1 @0.049s 2%: 1.0+4.0+0 ms clock, 16+1.0/4.0/6.0+0 ms cpu, 4->5->3 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
# command-line-arguments
gc 1 @0.012s 6%: 0.52+2.9+0 ms clock, 8.4+2.9/5.9/2.1+0 ms cpu, 4->6->5 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 2 @0.024s 3%: 0+11+0 ms clock, 0+0/2.1/12+0 ms cpu, 14->14->13 MB, 14 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 1 @0.116s 0%: 0+1.1+0 ms clock, 0+0.54/2.7/0+0 ms cpu, 3->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
协程栈
- 协程栈记录了协程的执行现场
- Go协程栈位于Go堆内存上
- Go使用参数拷贝传递函数的参数
- 3种特殊情况下,变量可能会逃逸到堆上
- Go模仿TCmalloc,建立了自己的堆内存架构
- 使用heapArena向操作系统申请内存
- 使用heapArena时,以mspan为单位,防止碎片化。
- mcentral是mspan们的中心索引
- mcache记录了分配给各个P的本地mspan。
堆内存分配
- Go将对象按照大小分为3种
- 微对象和小对象使用mcache
- mcache中的mspan填满后,与mcentral交换新的。
- mcentral不足时,在heapArena开辟新的mspan
- 大对象直接在heapArena开辟新的mspan。
并发GC
- 并发垃圾回收的关键在于标记安全
- Go语言采用混合屏障机制兼顾了安全与效率
GC优化
- GC主要由系统定时触发或者申请内存时触发,也可由程序员手动触发但不推荐这么做。
- GC优化的原则是减少在堆上产生垃圾。
- 使用GC分析工具可以帮助分析GC问题。