【八股】详细聊聊GOGC与GMP架构
1.GOGC
在golang中,运行时会定期采用三色标记法
对堆对象进行垃圾回收(gc),无需开发人员手动进行内存控制,简化了开发流程。本章我们从GOGC的历程讲起,详细聊聊GC的工作原理和GC的一些特性。
1.1.GOGC的发展历程
go 1.0~1.4:全STW的标记-清除
这个阶段是golang对垃圾回收的简单实现,采用经典的标记-删除法。在垃圾清除过程中,业务goroutine会被暂停。
- 标记阶段 1.暂停所有goroutine(STW) 2.从根对象(全局变量、栈变量)开始,递归标记所有可达对象
- 清除阶段 扫描堆,释放未标记的对象。
标记-清除法可以很安全的将所有未标记对象删除。得益于全STW,此种方法的实现也很简单,但相当于加了一把大锁,STW时间与堆大小成正比,可能导致
数百毫秒
的延迟,不适合低延迟应用。go 1.5:引入三色标记与并发GC
这个阶段的GC采用了三色标记算法,并将部分标记工作改为与应用程序goroutine并发执行,减少了STW时间
- 标记阶段并发化
1-大部分标记工作与goroutine并发执行 2-STW只用于初始化和结束阶段
- 写屏障 确保并发标记时,新分配或修改的对象不被遗漏
此优化将STW的时间从
数百毫秒
降低到几毫秒
go 1.8:进一步优化
此阶段清除操作可以并发执行或延后执行,进一步减少STW。同时,引入了GOGC参数(默认100),动态平衡内存使用和GC频率
1.2.三色标记法
- 对象颜色:
- 白色:未被标记
- 灰色:已发现但未完全扫描
- 黑色:已完成扫描
- 工作原理:
- 初始化(STW):标记所有根对象(全局变量、栈)为灰色。
- 并发标记:
- GC和应用goroutine并发执行
- 从灰色对象出发,扫描引用,将子对象标记为灰色,直到完成本轮所有灰色对象的扫描
- 重复执行,指导所有被指向的对象都经过了扫描
- 使用写屏障,记录应用协程对对象引用的修改
- 清除:并发或延迟清除白色对象(未被引用)
1.3.STW(Stop The World)
在最新的golang版本中,大幅减少了STW的时间,仅在初始扫描阶段与结束扫描阶段STW
- 初始扫描:扫描根对象,确保所有根对象都被置为灰色
- 结束扫描:结束扫描阶段,不再允许新的灰色对象被加入,避免GC线程无法真正结束。
- 其他:采用与应用goroutine并发策略
1.4.GC时机:GOGC变量与runtime.GC()
GC的时机可以通过golang的启动环境变量或运行时代码设置:
- GOGC环境变量:GOGC
- 运行时:runtime.GOGC(50)
不同GOGC策略适用于不同的场景
- 默认100:当对象堆大小+已分配的堆大小>上一次GC后的100%,触发GC,适用于大多数应用。
- 50-100:需要扫描的对象少,STW的时间短,适用于低延迟的应用,CPU占用会增加
- 200-500:高吞吐量的应用,例如批处理、数据处理,更少的GC触发,允许堆增长,且延迟不是主要关注点。
2.GMP调度
2.1.GMP调度中的核心概念
G(goroutine):协程,每个G可以有自己的栈(默认大小2MB),一个G表示一个函数调用,由于G是轻量级,因此支持百万级别的创建。
- P(Process):调度器,表示运行时可用的调度资源
- 默认大小=CPU核心数
- 有一个本地运行队列,队列存储待执行的G
- 解耦G和M,P不具备执行能力,需要将P与M绑定才可执行G,P负责G的管理与调度
M(Machine):OS级的线程,与P绑定后执行P本地队列中的G
本地队列:每个P都有一个本地队列,存放待执行的G
- 全局队列:全局共享的队列,当P的本地队列为空
2.2.Goroutine与操作系统线程的区别
特性 | goroutine | os线程 |
---|---|---|
创建开销 | 2KB | 1MB(500倍) |
数量支持 | 百万级别 | 几千个或更少 |
切换开销 | 用户态切换,无需进入内核态,开销小 | 上下文切换及涉及内核态,开销大 |
栈空间 | 初始小,动态增长 | 固定较大 |
系统调用 | 需要调度到真正OS线程(M)执行 | 原生支持 |
阻塞影响 | 会被P自动切出 | 阻塞影响整个线程 |
- G与M的M:N模型:多个G可以被绑定到P上,一个P可以被绑定到多个M上
- 使用GOMAXPROCX可以控制最大同时运行的线程数(即P的数量)
尽管goroutine是轻量的,但是也不能滥用goroutine,泄露或阻塞不退出的goroutine会消耗内存
2.3.状态切换与任务窃取
- 状态切换:G在运行时,可能会有两种阻塞原因
系统调度阻塞时:
1-G被放到其他P本地队列的队尾,若没有空闲的P本地队列,放到全局队列 2-解绑当前P与M,创建一个新的(或从缓存中获取一个新的)M执行P中的其他G
IO阻塞时:
1-G放到池子里(比如网络阻塞则放到NetPooler中),M不阻塞,继续执行P中剩下的G 2-待G中的IO完成后,G重新进入P的本地队列
- 任务窃取:平衡各个P之间的任务负载,确保每个CPU核心得到充分利用
- 优先本地队列:P优先从本地队列中取G来执行
- 从全局队列窃取:当本地队列没有任务时,从全局队列窃取任务放入本地队列
- 从其他P的本地队列窃取:当全局队列也没有G时,从其他P的本地队列窃取任务到本地执行
- 补充:go 1.4引入信号抢占
- 目的:强制让长时间占用CPU的G让出CPU
- 流程:如果某个G占用CPU时间过长(通常是10ms),调度器P会向M发送一个抢占信号。此操作系统会跳出G的执行,转而执行
信号函数
,在信号函数
中,P会保存当前G的执行上下文信息,并将当前G从M中移出,执行其他G