上篇说到超超从汇编的角度的解析了make和new的区别,下面来到了面试中常见的考点GC,Go的GC常常因为性能问题被业界所诟病,下面跟着超超来看看内存垃圾是如何产生的,以及Go从1.3到1.8在GC上做了哪些改进吧!

一、内存垃圾

面试官:你知道程序的垃圾是怎么产生的吗?

考点:Go内存管理

超超:程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。对于C++等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。在Go中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。

go语言内存结构 golang内存管理机制_go

垃圾是指程序向堆栈申请的内存空间,随着程序的运行已经不再使用这些内存空间,这时如果不释放他们就会造成垃圾也就是内存泄漏。例如下面这段程序

package main

//假设每个人都拥有自己都一部手机
type Person struct {
	phone *Phone
}

type Phone struct {
	money int
}

func main() {
	//定义一个Person为超超
	chao := new(Person)

	//超超一开始用的是iphone12
	iphone := &Phone{money: 6599}
	chao.phone = iphone

	//华为推出了鸿蒙,于是超超果断入了一部mate40
	huawei := &Phone{money: 5899}
	chao.phone = huawei

}

随着超超将手机从iPhone换成了华为,phone所指向的内存空间就变成了垃圾,这时就需要对phone指向的内存空间进行回收,否则就变成了内存泄漏。

go语言内存结构 golang内存管理机制_内存管理_02

二、Go的垃圾回收机制

面试官:那你来给我说说Go语言是如何实现GC的吧!

考点:GC的实现

超超:那我从Go1.3开始说起吧,

Go1.3使用的是标记清除法,分下面四步进行

  1. 进行STW(stop the worl即暂停程序业务逻辑),然后从main函数开始找到不可达的内存占用和可达的内存占用
  2. 开始标记,程序找出可达内存占用并做标记
  3. 标记结束清除未标记的内存占用
  4. 结束STW停止暂停,让程序继续运行,循环该过程直到main生命周期结束
  5. go语言内存结构 golang内存管理机制_go语言内存结构_03

一开始的做法是将垃圾清理结束时才停止STW,后来优化了方案将清理垃圾放到了STW之后,与程序运行同时进行,这样做减小了STW的时长。但是STW会暂停用户逻辑对程序的性能影响是非常大的,这种粒度的STW对于性能较高的程序还是无法接受,因此Go1.5采用了三色标记法优化了STW。

Go1.5三色标记法

三色标记算法将程序中的对象分成白色、黑色和灰色三类。白色对象表示暂无对象引用的潜在垃圾,其内存可能会被垃圾收集器回收;灰色对象表示活跃的对象,黑色到白色的中间状态,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;黑色对象表示活跃的对象,包括不存在引用外部指针的对象以及从根对象可达的对象。

go语言内存结构 golang内存管理机制_go语言内存结构_04

三色标记法分五步进行

  1. 将所有对象标记为白色
  2. 从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中
  3. 遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色
  4. 循环这个过程
  5. 直到灰色节点集合为空,回收所有的白色节点

go语言内存结构 golang内存管理机制_go_05


go语言内存结构 golang内存管理机制_go_06

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

go语言内存结构 golang内存管理机制_不变式_07

分析bug的根源所在,主要是因为程序在运行过程中出现了下面俩种情况

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

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

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

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

go语言内存结构 golang内存管理机制_go语言内存结构_08

屏障机制分为插入屏障和删除屏障,插入屏障实现的是强三色不变式,删除屏障则实现了弱三色不变式。值得注意的是为了保证栈的运行效率,屏障只对堆上的内存对象启用,栈上的内存会在GC结束后启用STW重新扫描。

插入屏障:对象被引用时触发的机制,当白色B对象被黑色A对象引用时,B对象被标记为灰色(栈上对象无插入屏障)。

go语言内存结构 golang内存管理机制_内存管理_09

缺点在于:如果对象1在栈上新创建了一个对象6,由于栈没有屏障机制,所以对象6仍为白色节点会被回收

go语言内存结构 golang内存管理机制_内存管理_10

所以栈在GC迭代结束时(没有灰色节点),会对栈执行STW,重新进行扫描清除白色节点。(STW时间为10-100ms)

删除屏障:对象被删除时触发的机制。如果灰色对象1引用的白色对象1被删除时,那么白色对象1会被标记为灰色。

go语言内存结构 golang内存管理机制_go_11

缺点:这种做法回收精度较低,一个对象即使被删除仍可以活过这一轮再下一轮被回收。(如果对象4没有引用对象3,此时对象3应该作为垃圾被回收,但是对象3却要等到下一轮GC才会被回收)

go语言内存结构 golang内存管理机制_go_12

同样也存在对栈的二次扫描影响程序的效率。

Go1.8 三色标记+混合写屏障

基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,所带来的性能瓶颈,Go在1.8引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步

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

下面为混合写屏障过程

go语言内存结构 golang内存管理机制_go语言内存结构_13

面试官:这个GC什么时候会被触发呢?

考点:GC细节

超超:触发GC有俩个条件,一是堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量GOGC,之后堆内存达到上一次垃圾收集的 2 倍时才会触发GC。二是如果一定时间内没有触发,就会触发新的循环,该触发条件由runtime.forcegcperiod变量控制,默认为 2 分钟。

面试官:说到这那我们再聊一下TCMalloc算法吧。

超超:好的(自动GC用的舒服,面试好难呀😶🌫️