栈内存(协程栈, 调用栈)
Go协程栈的作用
- 协程的执行路径
- 局部变量
- 函数传参
- 函数返回值
Go协程栈的位置
- Go的协程栈位于堆内存上
- Go堆内存位于操作系统虚拟内存上
Go栈帧的结构
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)
)
}
参数传递
- Go使用参数拷贝传递(值传递)
- 传递结构体时: 会拷贝结构体中全部内容
- 传递结构体指针时: 会拷贝结构体指针
总结
- 协程栈记录了协程的执行现场
- 协程栈还负责记录局部变量,传递参数和返回值
- Go使用参数拷贝传递
栈的扩增
初始栈帧大小: 2k~4k
局部变量太大
可以通过逃逸分析解决 逃逸分析
- 不是所有的变量都能放在协程栈上
- 栈帧回收后, 需要继续使用的变量
- 太大的变量 三种情形:
- 指针逃逸 函数返回了对象的指针
func a() *int {
v := 0
return &v
}
- 空接口逃逸 如果函数参数为interface{}, 函数的实参很可能会逃逸 因为interface{}类型的函数往往会使用反射(要求对象在堆上)
i := 1
fmt.Println(i) // 空接口逃逸
- 大变量逃逸 过大的变量会导致栈空间不足 在64位的机器中, 一般超过64KB的变量会逃逸
栈帧太多
栈扩容
- Go初始栈空间大小为2kb
- 在函数调用前判断栈空间(morestack)
- 必要时对栈进行扩容
- 早期使用分段栈, 后期使用连续栈
分段栈
- 1.13之前使用
- 优点: 没有空间浪费
- 缺点: 栈指针会在不连续的空间跳转
连续栈
直接开辟一块大小为原来两倍的新栈空间, 将老的全部拷贝过来; 空间使用率不足1/4是缩容, 变为原来1/2;
- 优点: 空间一直连续
- 缺点: 伸缩时开销大
总结
- 三种特殊情况(逃逸)下, 变量可能会分配到堆上
- 1.13之前, Go使用可伸缩的分段栈
- 1.14以后, Go使用连续栈, 伸缩时直接使用新栈
堆内存
操作系统的虚拟内存
- 不是Win的"虚拟内存"(交换区, Linux下的Swap)
- 操作系统给应用提供的虚拟内存空间
- 背后是物理内存, 也有可能是磁盘
- Linux获取虚拟内存: mmap, madvice
heapArena
- Go每次申请的虚拟内存单元为64MB
- 最多有2^20个虚拟内存单元
- 所有的heapArena组成了mheap(Go堆内存)
如何使用
- 线性分配: 没分配满的时候, 一直向后分配
- 链表分配: 使用链表串联起所有空闲内存块
线性分配或者链表分配很容易出现空间碎片
- 分级分配 找到比要分配对象的最小内存块
外部碎片比较少
级 -> mspan
内存管理单元mspan
- 根据隔离适应策略,使用内存时的最小单位为mspan
- 每个mspan为N个相同大小的“格子”
- Go中一共有67种mspan
- 每个heapArena中的mspan都不确定
- 如何快速找到所需的mspan级别? 中心索引mcenteral
中心索引mcentral
- 136个mcentral结构体, 其中
- 68个需要扫描的mspan()
- 68个不需要扫描的mspan(常量等)
mcentral的性能问题
- mcentral实际是中心索引, 使用互斥锁保护
- 高并发场景下, 锁冲突问题严重
- 参考协程GMP模型, 增加线程本地缓存
线程缓存mcache
- 每个P拥有一个mcache
- 一个mcache拥有136个mspan, 其中
- 68个需要GC扫描的mspan
- 68个不需要GC扫描的mspan
总结
- Go模仿TCmalloc, 建立了自己的堆内存架构
- 使用heapArena向操作系统申请内存
- 使用heapArena时, 以mspan为单位, 防止碎片化
- mcentral是mspan们的中心索引
- mcache记录了分配给各个P的本地mspan
对象分级
- Tiny微对象(0, 16B) 无指针
- Small小对象
[16B, 32KB] - Large大对象(32KB, +inf)
微小对象分配到普通mspan, class 1 ~ class 67 大对象量身定做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种
- 微小对象使用mcache
- mcache中的mspan填满后, 与mcentral交换新的
- mcentral不足时, 在heapArena开辟新的mspan
- 大对象直接在heapArena开辟新的mspan