Golang的内存模型与垃圾回收及其优化

栈内存

每个协程第一个栈帧为 goexit(),每次调用其他函数会加入一个栈帧

协程栈的作用:协程的执行路径
局部变量(方法内部声明 的变量)

Go的协程栈位于堆内存上(Go特殊的设计),栈的释放也是通过GC来释放的
C++,C他的栈区和堆区是分开的,堆上需要我们进行管理,栈上自行管理
Go的堆内存位于操作系统的虚拟内存上

用户的main方法首先会去找runtime.main的栈帧
然后开辟一块main.main的栈帧(这块区域大小是编译时系统分析得出来的)
这块栈帧首先记录栈基址(就是指从哪个方法调用进来的)方便返回的时候知道返回地址在哪
开辟调用方法的返回值,调用方法需要传入的参数,以及调用之后下一步的指令
return就是将返回值写回上一个栈帧预留的空间

返回之后将调用的方法栈帧回收
从这看出来Go采用的是参数拷贝传递(值传递)
传递函数体时会拷贝结构体中所有内容,建议传递结构体指针

栈帧太多,(调用方法太多),C++stackoverflow栈溢出

协程栈

协程栈相比线程很小(很轻量4K)
栈帧太多,局部变量太大(一个结构体中变量很多)?

局部变量太大
逃逸分析(从栈上逃到堆上去)
逃逸的两个原因
栈帧回收之后需要继续使用的变量,变量太大

指针逃逸,空接口逃逸,大变量逃逸
指针逃逸
函数返回了对象的指针
因为函数外面要访问这个地址,他不是一个局部变量,不能作为局部变量处理

空接口逃逸
函数的参数为空接口fmt.Println(i)
函数的实参 i 很可能会逃逸
因为 interface{} 类型的函数往往会使用反射判断一下类型,反射的对象要求在堆上

栈帧太多解决方法栈扩容,在函数调用前判断栈空间

分段栈
没有空间浪费
但栈指针会在不连续的空间跳转(开辟用完回收,又开辟又回收)

连续栈
空间不足,扩容2倍后拷贝
使用率不足1/4,空间减半

堆内存结构

mmap 是 linux 分配虚拟内存的手段
进程通过虚拟内存对应着物理内存
如果使用的虚拟内存太大(超过物理内存),OOM ,杀掉
go是一批一批获取虚拟内存的
heapArena
Go每次申请的虚拟内存单元64M,所有的heapArena组成了Go的堆内存mheap
相邻的heapArena可能对应不相邻的虚拟内存单元
相邻的虚拟内存单元可能对应不相邻的物理单元

heapArena线性分配,一直用之后的空间
容易出现空间碎片,增加分配花销
go采用分级分配(可以容纳的最小空间)
内存管理单元 mspan (span 跨度)
每个mspan 有N个相同 单元格子

在所有的heapArena快速找到所需 mspan 的级别
中心索引,目录(mcentral)

68中mspan
索引个数包括需要扫描的68 + 不需要扫描的68(一些常量)
mcentral 需要互斥锁保护,其中是一组span

所以采用GMP的思想,mcache
每个P(线程)拥有一个mcache
每个mcache包含了68*2 组span
每个P中mcache包含的span地址不同,不用考虑地址问题
如果写满的话,进行交换

p可以直接用type查找到,在runtime2包中
p中runq(本地队列),mcahe

堆内存分配

每个进程分配一块虚拟内存
对象分级
Tiny 0 - 16B 无指针 (微对象)
Small 16-32KB
Large 32-
微小对象分配到1-67span
大对象量身定做,分配到 0 span
1级span在17版本使用不到的
从mcache拿到2级span(16字节)
将多个微对象拼接合并成一个16字节放入

refill(跟中央缓存交换)
某种数量的mspan缺少时,会从heapArena开辟新的mspan(grow函数)
大对象还是先会去跟中央要,中央不够会从heapArena开辟新的mspan
heapArena不够会从操作系统虚拟内存申请新的heapArena

垃圾回收

标记-清除(别的语言可能会碎片化,但go不会,因为使用span分级)
标记-整理(整理开销,java老年代频率低可以用整理,使用span分级不需要整理)
标记-复制(减少了整理的开销,但是浪费空间,Java新生代,但是新空间是10份,所以浪费的空间比较小)

go选择最简单的标记清除
从哪开始找
被栈,全局变量,寄存器上的指针引用,上述变量被称为Root Set(GC root)
通过GC root 看可以到达哪些对象

串行GC
STW,回收,恢复协程运行
STW对性能影响,要把串行改成并行

并发的难点在于标记阶段
三色标记法
黑色 有用,已经分析扫描(对象及其中结构体中已经分析)
灰色 有用,还未分析扫描
白色 暂时无用(到最后要清除)
灰色为空时,清除白色,然后黑色变为白色,供下次扫描
三色标记为了之后的并发

并发标记问题
原本结构体中的指针变为nil
本来的nil指向了一个对象

针对堆对象
插入屏障(增加的引用为灰)
删除屏障(删除的引用为灰)
go采用的是混合屏障(插入+删除)
这样就变成了并发,提高效率

优化GC效率

系统定时触发(2min)
用户显示触发 runtime.GC
申请内存时触发 mallocgc(如果没有合适的内存可能触发GC)

GC优化原则
少在堆上分配垃圾
内存池化(channel 环形)
减少逃逸
使用空结构体(指向固定地址,不占用堆空间)
GC工具
可以go tool (pprof/trace)更多的是分析其他性能
也可以GODEBUG = “gctrace = 1”(简单粗暴)
$env
看看是否大于10%

空接口作为参数不会导致实参逃逸,但方法中使用反射的手段就很有可能导致实参逃逸
mcache 每个级别的 mspan 有 2 个,一个是 scan 类型,一个是 noscan 类型;scan 类型中含有指针,需要 GC 扫描,noscan 类型中不含指针,不需要 GC 扫描。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值