《Go专家编程》
垃圾回收算
业界常见的垃圾回收算法有以下几种
◎ 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减 1,当引用计数器为 0 时回收该对象。
优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阈值时才回收。缺点:不能很好地处理循环引用,而且实时维护引用计数也有一定的代价。代表语言:Python、PHP、Swift。◎ 标记—清除:从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有标记的对象被回收。
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW,即暂时停止程序运行。
- 代表语言:Go(其采用三色标记法)。
◎ 分代收集:按照对象生命周期的长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。
- 优点:回收性能好。
- 缺点:算法复杂。
- 代表语言:Java。
Go垃圾回收
1)垃圾回收的原理
简单地说,垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收,以供后续内存分配时使用。
下图展示了一段内存,内存中既有已分配的内存,也有未分配的内存,垃圾回收的目标就是把那些已经分配但没有对象引用的内存找出来并回收。

上图中,内存的 1、2、4 号位上的内存块已被分配(数字1 表示已被分配,0表示未分配)。变量 a、b 为指针,指向内存的 1、2 号位。内存块的4号位曾经被使用过,但现在没有任何对象引用了,就需要被回收。
垃圾回收开始时从 root 对象扫描,把 root对象引用的内存标记为“被引用”,考虑到内存块中存放的可能是指针,所以还需要递归地进行标记,全部标记完成后,只保留被标记的内存,未被标记的内存全部标记为未分配即完成了回收。
2)内存标记(Mark)
前面介绍内存分配时,介绍过 span 的数据结构,span 中维护了一个个内存块,并由一个位图 allocBits 表示每个内存块的分配情况。在span的数据结构中还有另一个位图gcmarkBits,用于标记内存块被引用的情况。
如下图所示,allocBits 记录了每块内存的分配情况,而gcmarkBits记录了每块内存的标记情况。标记阶段对每块内存进行标记,有对象引用的内存标记为1(如图中灰色部分所示),没有引用到的内存保持为 0(默认)。

allocBits 和 gcmarkBits 的数据结构是完全一样的,标记结束就是内存回收,回收时将allocBits 指向 gcmarkBits,代表标记过的内存才是存活的,gcmarkBits 则会在下次标记时重新分配内存,设计非常巧妙。
3)三色标记法
前面介绍了对象标记状态的存储方式,还需要有一个标记队列来存放待标记的对象,可以简单想象成把对象从标记队列中取出,将对象的引用状态标记在 span 的 gcmarkBits 中,把对象引用到的其他对象再放入队列。
三色只是为了叙述方便而抽象出来的一种说法,实际上对象并没有颜色之分。这里的三色对应了垃圾回收过程中对象的三种状态。
◎ 灰色:对象还在标记队列中等待。
◎ 黑色:对象已被标记,gcmarkBits 对应的位为1(该对象不会在本次GC中被清理)。
◎ 白色:对象未被标记,gcmarkBits 对应的位为0(该对象会在本次GC中被清理)。
例如,当前内存中有 A~F 共 6 个对象,根对象a、b本身为栈上分配的局部变量,根对象 a、b 分别引用了对象 A、B,而 B 对象又引用了对象D,则 GC 开始前各对象的状态如下图所示。

初始状态下所有对象都是白色的。
接着开始扫描根对象 a、b,如下图所示。

由于根对象引用了对象 A、B,那么 A、B变为灰色对象。接下来开始分析灰色对象,分析 A 时,A 没有引用其他对象,很快就转为黑色对象,B引用了 D,则 B 转入黑色的同时还需要将D转为灰色对象进行接下来的分析,如下图所示。

上图中的灰色对象只有 D,由于 D 没有引用其他对象,所以 D 转为黑色对象,标记过程结束,如下图所示。

最终,黑色对象会被保留下来,白色对象会被回收。
4)Stop The World
印度电影《苏丹》中描述摔跤的一句台词是:“所谓摔跤,就是把对手控制住,然后摔倒他”
对于垃圾回收来说,在回收过程中也需要控制内存的变化,否则在回收过程中指针传递会引起内存引用关系变化,如果错误地回收了还在使用的内存,那么结果将是灾难性的。
Go 中的 STW(Stop The World)就是停止所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复 goroutine。
STW 时间的长短直接影响了应用的执行,时间过长对于一些Web应用来说是不可接受的,这也是其广受诟病的原因之一。
垃圾回收优化
为了缩短 STW 的时间,Go 也在不断地优化垃圾回收算法。
1)写屏障(Write Barrier)
前面说过 STW 的目的是防止 GC 扫描时内存变化而停止 goroutine,而写屏障就是让goroutine 与 GC 同时运行的手段。虽然写屏障不能完全消除 STW,但是可以大大缩短 STW 的时间。
写屏障类似一种开关,在 GC 的特定时机开启,开启后指针传递时会标记指针,即本轮不回收,下次 GC 时再确定。GC 过程中新分配的内存会被立即标记,用的正是写屏障技术,即 GC 过程中分配的内存不会在本轮 GC 中回收。
2)辅助 GC(Mutator Assist)
为了防止内存分配过快,在 GC 执行过程中,如果 goroutine 需要分配内存,那么该goroutine会参与一部分 GC 的工作,即帮助 GC做一部分工作这个机制叫作Mutator Assist。
...

以上内容在《Go专家编程》均有详细介绍
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: 《Go专家编程》
本文地址: https://pptw.com/jishu/506334.html