引用自《Go GC 20 问》https://mp.weixin.qq.com/s/o2oMMh0PF5ZSoYD0XOBY2Q
含义
GarbageCollection 垃圾回收,一种自动内存管理机制
根集合,在GC时最先检查,包括:
全局变量
执行栈
寄存器
常见的GC方式
追踪式
标记清理:从根对象触发,标记清扫可回收的对象
标记整理:为了解决内存碎片,将对象尽可能整理到一块连续内存
增量式
标记与清扫分批进行,每次执行一小部分,增量推进,达到实时、无停顿的目的
增量整理:在增量式基础上增加对象整理过程
分代式
根据对象存活时间长短分类,时间短倾向于被回收,存活时间长的倾向于不回收
引用计数式
引用计数:引用计数值归零则立即回收
GoGC方式
无分代、不整理、并发的三色标记法
对象整理目的:
- 是解决内存碎片问题,但是Go给予tcmalloc分配算法,基本没有碎片问题。
另外顺序内存分配器在多线程并不适用,整理内存队tcmalloc分配没有实质提升 - 分代GC目标主要是针对新创建对象,不会频繁检查所有对象。但是Go会通过逃逸分析将大部分“新生”对象存储在栈上,需要长期保存的对象存在于堆中。栈会被回收,不需要GC。
Go的GC更专注于如何让GC和用户代码并发执行
三色标记法
核心:三色对象以及波面推进
当垃圾回收开始时,只有白色对象。随着标记过程开始进行时,灰色对象开始出现(着色),这时候波面便开始扩大。当一个对象的所有子节点均完成扫描时,会被着色为黑色。当整个堆遍历完成时,只剩下黑色和白色对象,这时的黑色对象为可达对象,即存活;而白色对象为不可达对象,即死亡。这个过程可以视为以灰色对象为波面,将黑色对象和白色对象分离,使波面不断向前推进,直到所有可达的灰色对象都变为黑色对象为止的过程。
颜色
- 白色(可能死亡):未被回收器访问的对象,所有对象在初始状态都是白色,回收结束后,白色对象会被回收
- 灰色(波面):已被访问的对象,但是回收器还需要队其中的指针扫描,因为可能还在指向白色对象
- 黑色(确定存活):已被回收器访问的对象,所有字段被扫描,黑色对象指针不能直接指向白色对象
STW
是指在垃圾回收过程中为了保证实现的正确性、防止无止境的内存增长等问题而不可避免的需要停止赋值器进一步操作对象图的一段过程。
例如启动一个routine运行无限for循环,其他逻辑代码执行runtime.GC()时,会一直卡在STW阶段
如今的STW停顿时间优化在半毫秒之内了,GC进入STW时需要等待让所有用户态代码停止,但是for{}所在的routine永远“不会被中断”从而停留在STW阶段。
实践中某一个routine长时间不停止,强制拖慢STW
但是go 1.14之后,这类的goroutine可以被异步的停止
观察GC
- 方法1:GODEBUG=gctrace=1 ./main
其他3个方法看文档:https://www.codercto.com/a/99611.html
有GC为什么还有内存泄漏
在GC语言中常说的内存泄漏:预期很快要被释放的内存,由于附着在长期存活的内存上、或者生命周期意外的延长,导致预计可以回收的内存无法被正常回收
- 预计快速释放的内存被根对象引用导致内存泄漏:例如内存被全局变量引用
- goroutine泄露:一个routine正常寿命周期不会释放上下文信息等。如果不停产生routine,而且不结束routine。
另外channel链接2个不同routine如果一个g向一个没有缓冲的channel发送数据,则改groutine会被永久休眠。
并发标记清除的难点
用户态代码在回收过程中会并发更新对象,所以就有了写屏障、混合写屏障。
什么时候会破坏垃圾回收的正确性?
- 赋值器修改对象,导致某一个黑色对象引用了白色对象
- 从灰色对象出发,到达白色对象的、未经访问过的路径被赋值器破坏
只要避免任意条件,则不会出现对象丢失:
条件1被避免,所有白色对象均被灰色对象引用,白色对象不会被遗漏
-
条件2被避免,即使白色对象被写入黑色对象中,但是从灰色对象出发总存在一条没有被访问过的路径,从而找到白色对象
在STW阶段对象赋值器触发写屏障,刷新对象。。。太复杂建议看原文。
Go 历史各个版本在 GC 方面的改进
Go 1:串行三色标记清扫
Go 1.3:并行清扫,标记过程需要 STW,停顿时间在约几百毫秒
Go 1.5:并发标记清扫,停顿时间在一百毫秒以内
Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在十毫秒以内
Go 1.7:停顿时间控制在两毫秒以内
Go 1.8:混合写屏障,停顿时间在半个毫秒左右
Go 1.9:彻底移除了栈的重扫描过程
Go 1.12:整合了两个阶段的 Mark Termination,但引入了一个严重的 GC Bug 至今未修(见问题 20),尚无该 Bug 对 GC 性能影响的报告
Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题
详细查看:https://www.codercto.com/a/99611.html
Go垃圾回收的API
runtime.GC:手动触发 GC
runtime.ReadMemStats:读取内存相关的统计信息,其中包含部分 GC 相关的统计信息
debug.FreeOSMemory:手动将内存归还给操作系统
debug.ReadGCStats:读取关于 GC 的相关统计信息
debug.SetGCPercent:设置 GOGC 调步变量
debug.SetMaxHeap(尚未发布):设置 Go 程序堆的上限值
GoGC如何调优
常见的GC问题
- 对停顿敏感,GC导致用户只需代码滞后
- 对资源消耗敏感,频繁分配内存应用,导致频繁垃圾回收,影响用户代码CPU利用
示例
- 合理化内存分配的速度、提高赋值器的 CPU 利用率,例如合理创建routine
- 降低并复用已经申请的内存
- 调整GOGC值:如果我们在遇到海量请求的时,为了避免 GC 频繁触发,可以通过将 GOGC 的值设置得更大,让 GC 触发的时间变得更晚,从而减少其触发频率
GC关注指标
- CPU利用率,标志回收算法会在多大程度上忒慢程序
- GC停顿时间,回收器会造成多长时间的停顿,STW、MarkAssist两部分
- GC停顿频率,STW、MarkAssist两部分
- GC可扩展性,堆内存变大时,回收器性能如何
如果内存分配速度超过了标记清除?
- 进入GC,进入并发标记阶段
- 并发标记会设置一个标记,并在mallocgc调用时进行检测
- 当存在新内存分配,会暂定分配过快的goroutine,将其转去执行一些辅助标记(Mark Assist)工作,从而达到放缓分配,辅助GC标记目的。
触发GC的时机
主动调用:runtime.GC触发,阻塞式调用等待GC运行完毕
被动触发:使用系统监控,2分钟没有产生GC则强制触发
使用步调算法,核心思想是控制内存增长比例
Go语言GC流程
1、GCMark:标记准备阶段,为并发标记做准备,启动写屏障。赋值器状态:STW
gcBgMarkPrepare
2、GCMark:扫描标记阶段,与赋值器并发执行,写屏障开启。赋值器状态:并发
gcBgMarkRootPrepare
3、GCMarkTermination:标记终止阶段,保证一个周期内标记任务完成。赋值器状态:STW
gcBgMarkTinyAllocs
4、GCoff:内存清扫阶段,将需要回收的内存还到堆中,写屏障关闭。赋值器状态:并发
gcBgMarkWorker
5、GCoff:内存归还阶段,将过多的内存还给操作系统,写屏障关闭。赋值器状态:并发
gcBgMarkDone
gcBgMarkTermination