栈内存(协程栈,调用栈)
- 为什么go的栈是在堆上?
go 使用的参数拷贝传递,如果传递的值比较大 注意传递其指针go 参数传递 使用 值传递, 也就是说传递结构体时候,拷贝结构体的指针,传递结构体指针时候 拷贝的结构体指针
- 协程栈的空间不够大 怎么办?
- 本地变量太大,栈帧太多
逃逸分析
- 不是所有的变量都放在协程栈上,栈帧回收后,需要继续使用的变量,或者 太大的变量,分为指针逃逸,空接口逃逸和大变量逃逸,从栈逃逸分配到堆空间上
函数返回的指针被其他使用
func a() *int {v :=0return &v // 导致 局部变量会分配在堆行 不会分配栈上
}
函数的参数是interface{} 函数的实参很可能会逃逸,主要因为interface{} 类型函数往往会使用反射
fmt.Println(i) // 入参属于interface{} 空接口, 是否有反射查看值是什么类型 逃逸到堆上
过大变量导致的空间不足,超过64KB的变量会逃逸
// 解决协程的栈空间不足
栈扩容
2. 解决方式 进行栈扩容,Go的栈初始空间为2KB
3. 在函数调用前判断栈空间morestack
4. 早期使用分段栈(go 1.13) 在逻辑上连接,优点没有空间浪费,栈指针会在不连续的空间跳转,后期 连续栈,缺点 伸缩时候开销大,扩容为原来2倍, 使用比例不足1/4, 变为原来的 1/2
go 堆内存
操作系统的虚拟内存:操作系统给应用提供的虚拟的内存的空间,背后也是物理内存或者磁盘
- go 使用 heapArena每次申请虚拟内存单元 64MB,所有的heapArena 组成 堆内存
- 线性分配据或者链表分配出现空间碎片,所有go 语言中使用分级分配,避免内存的碎片化,每个内存进行分级思想, mspan n内存管理单元
按照需求进行分级分配,runtime.sizeclass.go 进行分配, 总共有68 个级别。
其中 136 个span , mcentral 属于链接头,其中 68个需要GC扫描,其他68个不需要GC扫描。
mcentral 的属于中心索引,使用互斥锁保护,在高并发的场景下 锁冲突严重,参考GMP模型,增加线程的本地缓存。
- Go 模仿TCmalloc ,建立自己的堆内存架构
- 使用heapArena 向操作系统申请内存,以mspan 为单位,防止碎片化
- mcentral 是mspan 的中心索引
- 使用mcache 本地缓存 大大降低 锁竞争问题
堆如何进行分配
无指针
go 语言对象的垃圾回收
1. 被栈上的指针引用 2.被全局变量引用 3.被寄存器中指针引用
如何减少GC对性能的分析
三色标记方法Yuasa 删除屏障, 强制将释放的C指针变成灰色,避免 在GC过程中被粗我䣌标记s释放的指针进行强制为灰色Dijkstra 插入屏障
- 被删除的堆对象标记成为灰色
- 被添加的堆对象标记成为灰色
并发垃圾回收关键在于标记安全,兼顾的安全的效率
GC 优化效率
GC触发的时机
-
系统定时触发
g0 协程内的sysmon 定时检查 ,在2min 内 forcegcperiod 没有过GC,触发,谨慎调整 -
用户显示触发
调用runtime.gc 并不推荐 -
申请内存触发
给申请对象的时候伴随着GC -
GC优化原则
- 尽量少在堆上产生垃圾
- 内存池化(channel 中 环形池)
- 减少逃逸 (fmt 包, 返回了指针不是拷贝)
- 使用空结构体 (不占用空结构体,使用channel 传递空结构体)
使用如下的方式 查看内存
$env:GODEBUG="gctrace=1"