Go中的内存模型与垃圾回收(一)| 青训营笔记

109 阅读4分钟

栈内存(协程栈, 调用栈)

Go协程栈的作用

Pasted image 20230524101355.png

  • 协程的执行路径
  • 局部变量
  • 函数传参
  • 函数返回值

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)
	)
}

Pasted image 20230524102337.png

参数传递

  • 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之前使用
  • 优点: 没有空间浪费
  • 缺点: 栈指针会在不连续的空间跳转

Pasted image 20230524104520.png

连续栈

直接开辟一块大小为原来两倍的新栈空间, 将老的全部拷贝过来; 空间使用率不足1/4是缩容, 变为原来1/2;

  • 优点: 空间一直连续
  • 缺点: 伸缩时开销大

总结

  • 三种特殊情况(逃逸)下, 变量可能会分配到堆上
  • 1.13之前, Go使用可伸缩的分段栈
  • 1.14以后, Go使用连续栈, 伸缩时直接使用新栈

堆内存

操作系统的虚拟内存

  • 不是Win的"虚拟内存"(交换区, Linux下的Swap)
  • 操作系统给应用提供的虚拟内存空间
  • 背后是物理内存, 也有可能是磁盘
  • Linux获取虚拟内存: mmap, madvice

Pasted image 20230524111556.png

heapArena

  • Go每次申请的虚拟内存单元为64MB
  • 最多有2^20个虚拟内存单元
  • 所有的heapArena组成了mheap(Go堆内存)

如何使用

  • 线性分配: 没分配满的时候, 一直向后分配

Pasted image 20230524112720.png

  • 链表分配: 使用链表串联起所有空闲内存块

Pasted image 20230524112807.png

线性分配或者链表分配很容易出现空间碎片

  • 分级分配 找到比要分配对象的最小内存块

Pasted image 20230524113122.png 外部碎片比较少 级 -> mspan

内存管理单元mspan

  • 根据隔离适应策略,使用内存时的最小单位为mspan
  • 每个mspan为N个相同大小的“格子”
  • Go中一共有67种mspan

Pasted image 20230524113424.png

  • 每个heapArena中的mspan都不确定
  • 如何快速找到所需的mspan级别? 中心索引mcenteral

中心索引mcentral

  • 136个mcentral结构体, 其中
    • 68个需要扫描的mspan()
    • 68个不需要扫描的mspan(常量等)

Pasted image 20230524123452.png

mcentral的性能问题

  • mcentral实际是中心索引, 使用互斥锁保护
  • 高并发场景下, 锁冲突问题严重
  • 参考协程GMP模型, 增加线程本地缓存

线程缓存mcache

  • 每个P拥有一个mcache
  • 一个mcache拥有136个mspan, 其中
    • 68个需要GC扫描的mspan
    • 68个不需要GC扫描的mspan

Pasted image 20230524124322.png

总结

  • 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存入

Pasted image 20230524125614.png

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