图解Go的垃圾回收机制

一、内存垃圾的是怎样产生的?

  • 程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。
  • 对于某些早期的编程语言栈上的内存由编译器管理回收,堆上的内存空间需要程序员负责申请与释放。
  • 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就可以增量式的运行,减少停顿时间。

  • 什么是三色标记?

    1. 黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。
    2. 灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。
    3. 白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。

    三色标记规则:黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。

  • 三色标记的主要流程:

    1. 初始所有对象被标记为白色。
    2. 寻找所有Root对象,比如被线程直接引用的对象,把Root对象标记为灰色。
    3. 把灰色对象标记为黑色,并它们引用的对象标记为灰色。
    4. 持续遍历每一个灰色对象,直到没有灰色对象。
    5. 剩余白色对象为垃圾对象。
      请添加图片描述

​ 这种方法看似很好,但是将GC和程序会放一起执行,会因为cpu的调度出现下面这种情况,导致被引用的对象3会被垃圾回收掉,从而出现错误。

请添加图片描述

分析分析上述存在Bug的根源,主要有以下两种情况

  1. 一个白色对象被黑色对象引用
  2. 灰色对象与它之间的可达关系的白色对象遭到破坏

因此在此基础上拓展出了俩种方法,强三色不变式和弱三色不变式

  1. 强三色不变式:不允许黑色对象引用白色对象
  2. 弱三色不变式:黑色对象可以引用白色,白色对象存在其他灰色对象对他的引用,或者他的链路上存在灰色对象

为了实现这俩种不变式的设计思想,从而引出了屏障机制,即在程序的执行过程中加一个判断机制,满足判断机制则执行回调函数。

  • 写屏障(屏障机制分为插入屏障和删除屏障)

    插入屏障实现的是强三色不变式,删除屏障则实现了弱三色不变式。值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在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引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步

  1. GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)
  2. GC期间,任何栈上创建的新对象均为黑色
  3. 被删除引用的对象标记为灰色
  4. 被添加引用的对象标记为灰色

下面为混合写屏障过程:

请添加图片描述

  • 并发标记

    Go的垃圾回收都为每个P都分配了一个gcMarker协程,用于并发标记对象,这样有些P在标记对象,而有些P上继续运行用户协程。

    Go的并发标记有4种运行模式,这里不深入研究,这里举一个并发标记的场景:在goroutine的调度过程中,如果当前P上已经没有g可以执行,也偷不到g时,P就空闲下来了,这时候可以运行当前P的gcMarK-er协程。(具体可以看看协程调度的GMP模型,本地对队列P绑定M线程,执行P中的g。)

  • 触发GC(三种方式)

    1. 辅助GC

      在分配内存时,会判断当前的Heap(堆内存)内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置,一般是之后的堆内存达到上一次垃圾收集的2倍时才会触发GC),如果超过阈值,则会启动一轮GC。

    2. 调用runtime.GC()强制启动一轮GC

    3. 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的垃圾回收机制,着重讲解了三色标记法和写屏障的过程。

  • 7
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值