这篇主要简单分享Go内存管理和协程调度相关:
Go 堆栈解析
内存逃逸
make 和new 区别
Go GC
GMP 协程调度模型
Go 堆栈解析
GO 进程的运行也需要堆栈内存,且Go 的运行是直接运行在操作系统上的,并没有如java 一样运行在虚拟机中,所以,Go的内存模型和C/C++ 上差别不大,仅有的区别时对于堆栈内存的利用上而已,比如Go 的内存逃逸等.
Go 的内存分配
Go 的内存分配系统是基于TCmalloc 实现的,所以大概分配机制也和TCmalloc 差不多。这里,我们先介绍几个Go 中内存池分块概念:
page: 操作系统中的内存页
Span: 以链表形式构建起来相同大小的页组(块)单元
mcache: 一个P对应一个 mcache, mcache 里有个种size 大小的span,也是由链表串链而成的span 池
mcentral: 和mcache 一样的结构,是所有P 的共享内存池,是每个P mcache 的兜底,访问需要加锁
mheap由span组成的页块堆,堆单元是span -- 对应 TCmalloc 中的 Page heap
分配规则如下:
分配优先级 (堆栈内存块分配大致套路):
mcache > mcentral > mheap > os
内存分配机制(按大小确定分配机制):
0 < 16B : mcache.tiny -> mcache
16B <= 32 k :mcache -> mcentral -> p.pagecache -> mheap - > os
> 32 k : p.pagecache -> mheap -> os
一文彻底理解Go语言栈内存/堆内存本文将从如下6个方向层层递进,帮助大家彻底理解Go语言的栈内存和堆内存。 - 计算机为 - 掘金
内存逃逸
内存逃逸发生在编译期,编译器会根据变量声明周期已经内存申请请款决定对象在栈或者堆中初始化。所谓的内存逃逸是相对于C/C++ 来说的,就是本该分配在栈上的对象因为生命周期的延续使得该对象最终在堆中初始化。
常见内存逃逸
指针逃逸,返回一个函数内实例化的变量的地址,供函数外使用 (函数返回值是指针,返回了一个在 局部变量的内存地址)
分配大对象,对象大到栈无法分配,此时会分配到堆中 -- 对于 C/C++ 会报栈溢出
调用 带由泛型接口参数的函数,因为泛型接口 无法确定对象大小,所以索性分配到堆中
大小不能再编译期确定的。
map\sync.map、slice、string、channel
map/sync.map、slice .append(...) 扩容
如何避免内存逃逸
除非必要的做抽象泛型,否则明确参数类型
明确大小,如果能指定大小以后不扩容的,可以使用数组,而不用切片
使用
go build --gcflags ‘-m -l' test.go
可以查看编译器决定哪些对象会内存逃逸
make 和new 区别
二者都是在堆中申请内存
new :用于实例化一个对象并规整化这个对象的内存(不会由垃圾数据,会讲内存值都置0),返回对象内存地址. 不常用,可用
:=
代替make: 用于chan、map、slice 初始化,不可代替。在使用chan/map/slice 前必须先 make 否则 painc
Go GC
GC 即是垃圾回收,说的是无用内存的回收。
为什么要GC?降低开发者开发负担,帮助开发者聚焦于业务开发,不用再像C/C++ 那样区手动管理内存的分配和回收。
缺点?好用GC 系统通常设计复杂,且不如手动管理来的灵活,再GC 都需要SWT,会使程序出现性能抖动。
常见 GC 算法
引用计数法
每个内存块有一个计数器,初始化为1 被引用计数器 +1,删除引用 -1, 计数器 = 0 时,回收内存
优点:实现简单
缺点:循环引用的内存块无法被回收。
标记清除法
从根节点开始,DFS访问所有可达内存块并作活跃标记,遍历完成后,没有被标记的内存块即为可回收内存块,回收...
优点:实现简单,快速,只需遍历一次即可发现待回收块
缺点:容易产生内存碎片,申请大内存块时可能出现没有足够大的内存,导致无法实例化
复制法
将内存二等分,触发GC后将活跃内存块复制到另一半内存中,然后回收上次使用的内存块
优点:实现简单
缺点:内存利用率低,浪费内存
标记整理
从根节点开始扫描,所有活跃内存块按内存地址排序被规整移动到以块内存中并更新引用地址,清理规整之后,未使用的内存块。
分代法
根据对象的活跃周期进行分区划分,不同内存分区 使用不同内存回收算法:
年轻代区域:生命周期短,内存块经常被大量销毁,只有少量对象存活,内存对象一般较小 --- 常用复制法
老年代区域:生命周期长,很少被销毁,常用标记清理法\标记整理法
Go 中的内存管理
Go 使用的内存管理算法,是一种混合算法 -- 三色标记法(黑、灰、白) + 删写屏障 实现的内存管理。
Go 为什么没有选用标记整理或者复制
这两算法最大优点都是能够通过内存整理使内存规整化减少内存碎片,但是Go 的内粗分配系统是类似TCmalloc 的实现,已经由针对内存碎片的优化处理,所以使用这两算法收益不大。
Go 为什么不适用分代
因为Go 有内存逃逸机制,所以并没大量的短周期对象需要频繁大量销毁,所以分代收益不大。
Go 改进的三色标记法
三色不变性约束
首先需要引入三色标记法的两大约束,这两大约束可以保证内存被正确回收 (也就是屏障技术):
强三色不变性: 黑色对象不能指向白色(但可以指向灰色、黑色),在不符合弱三色不变性不成立的情况下.
弱三色不变性: 黑色对象指向白色对象时,必须有灰色对象在连通图中,保护白色对象不被回收
内存屏障技术
什么时内存屏障技术?编译器生成hook 代码,拦截写操作,做一些标记操作(做插入/删除 写屏障操作),以保证三色不变性约束,保证内存块正确被回收,不出错。
插入写屏障
新内存对象(初始化色,白色)加入内存管理链时,新对象染色为灰色
触发时机,新对象堆栈中生成
删除写屏障
被删除对象(前继对象指向自身的指针被删除)如果自身是灰色或白色,染色为灰色
触发时机,对象被删除
混合写屏障
插入写屏障 + 删除写屏障
Go三色标记法 内存回收步骤
堆对象三色标记
遍历开始前,所有对象默认白色,发生出入写屏障对象灰色,发生删除写对象灰色
从根开始遍历第一层对象,将对象置为黑色(染色标记位为1,不在灰色队列中)
从黑色对象开始向下遍历一层,将遍历到的对象染为灰色(染色标记位为1,加入灰色队列中)
从灰色对象队列开始遍历,起始节点染为黑色,向下遍历一层,将遍历到的对象染为灰色加入灰色队列中
重复4,直到灰色队列为空,没有对象可加入,开始回收白色对象(染色标记位为0,不在灰色队列中)。
栈对象三色标记
从根节点触发,做DFS,所有遍历到的对象全然为黑色。--- 因为栈对象会自动销毁,所以不用GC 系统主动回收
小结
插入写屏障没有保证所有内存对象的强三色不变性,考虑到性能的影响栈对象不使用插入写屏障
混合写屏障不做全局SWT,并未不是完全并行,扫描的时候时逐个暂停的。
GMP 协程调度模型
在GMP 模型如何工作之前,先引入相关概念:
G、M、P
G : 待执行得Go rountine
mian.G0 拉起main.rountine G0,负责启动调度器任务
Local.G0 每个M 都会绑定一个本地G0,负责调度,获取P,从全局G队列中获取G 或者从友商P中获取后半部分队列中得G,执行完一个本地G后会切换local.G0 从本地P中拉一个G切换执行,继续套娃...
M
M0 内核线程实际G0 得真正执行者
local.M 每个P 背后的真正工作线程
P:存G 的队列,抢占特性每个G只有10ms 时间片,保证G 生成的G 的局部性。
开启调度流程
全局队列
全局待执行 G 队列,访问需加锁,本地P满了,会放把溢出的G放到全局G队列中
全局空闲 P 队列,空闲没有绑定G的P休眠地
全局 空闲-休眠 M 队列 ,没有找到P的休眠地
五大约束
抢占模式
P中的G 每个都分配10ms
work stealing
本地P为空无G可执行,会切换到本地G0,从群居队列中上锁拉部分G执行,如果全局G队列为空,从友商P中后半部分队列中偷一半向下取整,打乱加入到本地P中,调度执行
handle off
系统调用解绑P
因为系统待用要陷入内核态,工作到切换回用户态再到调度器这个过程是十分耗费时间片的,所以解绑,充分利用资源,让更多G 得到调度执行
非阻塞系统调用解绑P,记忆原配P,恢复到用户态后切换G0找原配P,能拉回啦继续执行P中任务,不能拉回来则去全局空闲P队列中来一个P来执行,如果一个有G的P也找不到则把返回用户态的G标记为待执行,放回全局G队列中,M放到全局休眠队列,休眠
阻塞移除P,唤醒,返回用户态,切换G0找P
G 局部性
为了局部性,G生成的G 直接加入本地P队列,满了扔全局队列
创建唤醒 --- 叫醒服务...
新的创建G后,回去唤醒休眠中的M去找P执行任务...
模型概览
调度流程
请根据上面引入的概念和约束,自圆其说...