一、内存垃圾的是怎样产生的?
- 程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。
- 对于某些早期的编程语言栈上的内存由编译器管理回收,堆上的内存空间需要程序员负责申请与释放。
- Go中的栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收。
- 垃圾是指程序向堆栈申请的内存空间,随着程序的运行已经不再使用这些内存空间,这时如果不释放他们就会造成垃圾也就是内存泄漏。
下面我们举一个栗子,程序是怎样产生垃圾的
package main
// 假设每个人都有自己的手机
type Person struct {
phone *Phone
}
type Phone struct {
money int
}
func main() {
// 我们定义一个Person为芋圆
yuyuan := new(Person)
// 芋圆刚开始喜欢 iphone13,用的就是 iphone13
iphone := &Phone{money:6999}
yuyuan.phone = iphone
// 芋圆后面又喜欢上了华为手机,于是又立刻换成了华为mate系列
huawei := &Phone{money:5999}
yuyuan.phone = huawei
}
随着芋圆将手机从iPhone换成了华为,芋圆的手机先前只想的iphone内存空间就成了垃圾,这时候就需要对phone只想的内存空间进行回收,否则就会造成内存泄漏。
二、Golang的垃圾回收机制
2.1、Go垃圾回收发展史
- go1.1,提高效率和垃圾回收精确度。
- go.13,提高了垃圾回收的精确度。
- go1.4,之前版本的runtime大部分是使用C写的,这个版本大量使用Go进行了重写,让GC有了扫描stack的能力,进一步提高了垃圾回收的精确度。
- go1.5,目标是降低GC延迟,采用了并发标记和并发清除,三色标记,write barrier,以及实现了更好的回收器调度,设计文档1,文档2,以及这个版本的[Go talk]。
- go1.6,小优化,当程序使用大量内存时,GC暂停时间有所降低。
- go1.7,小优化,当程序有大量空闲goroutine,stack大小波动比较大时,GC暂停时间有显著降低。
- go1.8,write barrier切换到hybrid write barrier,以消除STW中的re-scan,把STW的最差情况降低到50us,设计文档。
- go1.9,提升指标比较多,1)过去
runtime.GC
,debug.SetGCPercent
, 和debug.FreeOSMemory
都不能触发并发GC,他们触发的GC都是阻塞的,go1.9可以了,变成了在垃圾回收之前只阻塞调用GC的goroutine。2)debug.SetGCPercent
只在有必要的情况下才会触发GC。 - go.1.10,小优化,加速了GC,程序应当运行更快一点点。
- go1.12,显著提高了堆内存存在大碎片情况下的sweeping性能,能够降低GC后立即分配内存的延迟。
- 比较注意的是从go1.5开始使用的并发标记和三色标记法、写屏障为主要,再有就是从1.8之后的混合写屏障是比较重大的改动。
2.2、常见的垃圾回收算法
- 引用计数:每个对象维护一个引用计数,当被引用对象被创建或被赋值给其他对象时引用计数自动 +1。如果这个对象被销毁,那么计数-1,当计数为0时,回收该对象。
- 优点:对象可以很快被回收,不会出现内存耗尽或者达到阈值才回收。
- 缺点:不能很好的处理循环引用。
- 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有标记的则进行回收。
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW(stop the world),暂时停止程序运行。
- 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
- 优点:回收性能好
- 缺点:算法复杂
2.3、go1.3使用的是标记清除法
- 进行STW(stop the worl即暂停程序业务逻辑),然后从main函数开始找到不可达的内存占用和可达的内存占用
- 开始标记,程序找出可达内存占用并做标记
- 标记结束清除未标记的内存占用
- 结束STW停止暂停,让程序继续运行,循环该过程直到main生命周期结束
2.3、三色标记法(go1.5垃圾回收原理)
-
为什么需要三色标记?
三色标记的目的,主要是利用Tracing GC做增量式垃圾回收,降低最大暂停时间。原生Tracing GC只有黑色和白色,没有中间的状态,这就要求GC扫描过程必须一次性完成,得到最后的黑色和白色对象。在前面增量式GC中介绍到了,这种方式会存在较大的暂停时间。
三色标记增加了中间状态灰色,增量式GC运行过程中,应用线程的运行可能改变了对象引用树,只要让黑色对象直接引用白色对象,GC就可以增量式的运行,减少停顿时间。
-
什么是三色标记?
- 黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。
- 灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。
- 白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。
三色标记规则:黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。
-
三色标记的主要流程:
- 初始所有对象被标记为白色。
- 寻找所有Root对象,比如被线程直接引用的对象,把Root对象标记为灰色。
- 把灰色对象标记为黑色,并它们引用的对象标记为灰色。
- 持续遍历每一个灰色对象,直到没有灰色对象。
- 剩余白色对象为垃圾对象。
这种方法看似很好,但是将GC和程序会放一起执行,会因为cpu的调度出现下面这种情况,导致被引用的对象3会被垃圾回收掉,从而出现错误。
分析分析上述存在Bug的根源,主要有以下两种情况
- 一个白色对象被黑色对象引用
- 灰色对象与它之间的可达关系的白色对象遭到破坏
因此在此基础上拓展出了俩种方法,强三色不变式和弱三色不变式
- 强三色不变式:不允许黑色对象引用白色对象
- 弱三色不变式:黑色对象可以引用白色,白色对象存在其他灰色对象对他的引用,或者他的链路上存在灰色对象
为了实现这俩种不变式的设计思想,从而引出了屏障机制,即在程序的执行过程中加一个判断机制,满足判断机制则执行回调函数。
-
写屏障(屏障机制分为插入屏障和删除屏障)
插入屏障实现的是强三色不变式,删除屏障则实现了弱三色不变式。值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在GC结束后启用STW重新扫描。
我们结合一段用户代码介绍写屏障,也是对上述Bug的一种代码表示:
A.Next = B A.Next = &C{}
三色标记的扫描线程是跟用户线程并发执行的,考虑这种情况:
用户线程执行完
A.Next = B
后,扫描线程把A标记为黑色,B标记为灰色,用户线程执行A.Next = &C{}
,C是新对象,被标记为白色,由于A已经被扫描,不会重复扫描,所以C不会被标记为灰色,造成了黑色对象指向白色对象的情况,这种三色标记中是不允许的,结果是C被认为是垃圾对象,最终被清扫掉,当访问C时会造成非法内存访问而Panic。
写屏障可以解决这个问题,当对象引用树发生改变时,即对象指向关系发生变化时,将被指向的对 象标记为灰色,维护了三色标记的约束:黑色对象不能直接引用白色对象,这避免了使用中的对象被释放。
有写屏障后,用户线程执行 A.Next = &C{}
后,写屏障把C标记为灰色。
2.4、Go1.8三色标记 + 混合写屏障
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,所带来的性能瓶颈,Go在1.8引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步
- GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)
- GC期间,任何栈上创建的新对象均为黑色
- 被删除引用的对象标记为灰色
- 被添加引用的对象标记为灰色
下面为混合写屏障过程:
-
并发标记
Go的垃圾回收都为每个P都分配了一个gcMarker协程,用于并发标记对象,这样有些P在标记对象,而有些P上继续运行用户协程。
Go的并发标记有4种运行模式,这里不深入研究,这里举一个并发标记的场景:在goroutine的调度过程中,如果当前P上已经没有g可以执行,也偷不到g时,P就空闲下来了,这时候可以运行当前P的gcMarK-er协程。(具体可以看看协程调度的GMP模型,本地对队列P绑定M线程,执行P中的g。)
-
触发GC(三种方式)
-
辅助GC
在分配内存时,会判断当前的Heap(堆内存)内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置,一般是之后的堆内存达到上一次垃圾收集的2倍时才会触发GC),如果超过阈值,则会启动一轮GC。
-
调用
runtime.GC()
强制启动一轮GC -
sysmon是运行时的守护进程,当超过
runtime.forcegcperiod
(默认值是2分钟)没有运行GC会启动一轮GC
-
-
GC调节参数
Go垃圾回收不像Java垃圾回收那样,有很多参数可供调节,Go为了保证使用GC的简洁性,只提供了 一个参数GOGC
。
GOGC
代表了占用中的内存增长比率,达到该比率时应当触发1次GC,该参数可以通过环境变量设置。
该参数取值范围为0~100,默认值是100,单位是百分比。
假如当前的heap占用内存时为3MB,GOGC = 75
5 * (1 + 75%) = 8.75MB
等heap占用内存大小达到8.75MB会触发1轮GC。
GOGC
还有两个特殊值:
1、“off”:代表关闭GC。
2、0:代表持续进行垃圾回收,只用于调试。
总结
本文主要介绍了内存的垃圾是怎样回收的,Go的垃圾回收机制,着重讲解了三色标记法和写屏障的过程。