最近垃圾分类的话题热度一下子就上去了,很多人因为垃圾分类的问题很头痛。因为垃圾这个话题,那我就想来说说Golang里面的垃圾,于是就有了这篇博客,golang中的垃圾回收。

现阶段网上针对golang垃圾回收的解析已经很多了,所以我也没有必要仔仔细细的一点点说,还是那个原则,用最直白的话告诉你,垃圾到底是怎么收的。

GC的意义

首先本文后续都会使用 GC 代替垃圾回收这几个字。
我们知道创建对象会给他分配内存资源,如果这个对象不使用了,而这个内存资源却一直被占用的话,那么我们的电脑很快就会被放满,所以需要将这些垃圾对象进行回收。

什么才是垃圾

要回收,那么我们必须知道什么才是垃圾,什么不是垃圾。
在我们看来,一个对象以后都不用了,就是垃圾。
在程序看来,一个对象没有被引用了,就是垃圾。

GC的流程

首先说明一下,下面说的停,都是STW,stop the world,全世界暂停,所有运行的都停下来了。
这个流程可以由下面这张图展示:
-w1183

第一个过程

先告诉所有人,停一下,我来记录一下当前状态。

第二个过程

告诉所有人,你们继续,该干嘛干嘛,我标记一下要用的对象

一开始所有点是白色,首先从根节点出发,标记相连的点为灰色(相连证明有引用),并且将所有灰色的点存起来;

然后遍历所有灰色的点,标记所有灰色的点相连的点为灰色,并且将自己标记为黑色;然后重复,直到没有点是灰色;

第三个过程

告诉所有人,再停一下,在第二个过程中,因为所有人继续在工作,那么就会产生新的垃圾,因为第一个过程记录了状态,所以需要标记一下新的垃圾;然后清除所有白色的点,因为白色的点是没人引用的,也就是垃圾。

为什么要这样GC

你一定会有这样的疑问:

  • 为什么要暂停两次?
  • 为什么不直接标记?
  1. 如果再标记的过程中不断的在创建新的对象,那么永远就标记不完了。
  2. 如果标记的过程中,原来的被标记的对象引用发生变更也会导致问题。

那么既然会导致那么多问题,为什么不直接停下来,标记完回收完了再开始呢?
因为慢~

所以这样GC的原因是既要保证GC正常执行,又要保证效率,不能停的时间太长。

第一个过程

其实第一次停的时候,启动了一个写屏障 (write barrier)它需要记录后续过程中新创建的对象

第二个过程

这个过程称为三色标记,有点类似广度优先搜索。

第三个过程

这次是必须停,因为在第二个过程中引用会发生变化,从而需要停止后重新扫描一遍;然后关闭写屏障,最后再清理。

重 点

  • 什么时候需要stw?

开启写屏障时需要stw
关闭写屏障前需要stw


  • 什么时候是并发执行的?

开启写屏障之后的标记过程与其他程序并发执行
关闭写屏障之后的清扫过程与其他程序并发执行

GC的触发条件

那毕竟GC还是需要STW的,虽然可能停止时间很短,但是对于程序来说,整个程序停止1秒那对于用户来说就是致命打击。所以GC肯定需要一个触发的条件,不能想来就来。

GC百分比

这是一个触发的条件,默认GC百分比设置的是100,意思是,如果这次回收之后总共占用2M的内存,那么下次触发的条件时当超过4M的时候;同理,当这次回收之后总共占用4M,那么下次触发条件就是8M。

2分钟

这个简单,当一定时间(2分钟)没有执行过GC就触发GC(称为GC forced)
监控服务 sysmon 每隔 2 分钟就会检查一次垃圾 回收状态,如超出 2 分钟未曾触发,那就强制执行。

手动

使用命令runtime.GC()手动触发GC

如何查询gc过程

启动时加上命令GODEBUG="gctrace=1"
运行一段时间你会看到类似下面的输出

1
2
3
4
5
gc 1 @351.002s 0%: 0.017+0.88+0.038 ms clock, 0.21+0/0.75/0.72+0.45 ms cpu, 4->4->2 MB, 5 MB goal, 12 P
gc 2 @351.022s 0%: 0.006+1.6+0.045 ms clock, 0.075+0/0.57/0.65+0.54 ms cpu, 8->8->7 MB, 9 MB goal, 12 P
gc 3 @351.030s 0%: 0.005+2.7+0.032 ms clock, 0.061+0/3.0/0.90+0.38 ms cpu, 15->15->13 MB, 16 MB goal, 12 P
gc 4 @351.042s 0%: 0.005+5.7+0.043 ms clock, 0.067+0/0.44/5.6+0.52 ms cpu, 29->29->25 MB, 30 MB goal, 12 P
gc 5 @351.069s 0%: 0.005+10+0.039 ms clock, 0.063+0/0.33/10+0.47 ms cpu, 57->57->49 MB, 58 MB goal, 12 P
  • 1 表示第一次执行
  • @351.002s 表示程序执行的总时间
  • 0% 垃圾回收时间占用的百分比
  • 0.017+0.88+0.038 ms clock 垃圾回收的时间,分别
    stop-the-world (STW) sweep termination + concurrent mark and scan + and STW mark termination
    0.017表示mark阶段stw的时间,是第一次stw,短暂的那个。
    0.88表示并发标记和扫描的时间。
    0.038表示mark termination中stw的时间,第二次stw,长的那个。
  • 0.10+0.23/5.4/12+0.40 ms cpu:按顺序分成三部分,0.10表示整个进程在mark阶段STW停顿时间(0.013 * 8);0.23/5.4/12有三块信息,0.23是mutator assists占用的时间,5.4是dedicated mark workers+fractional mark worker占用的时间,12是idle mark workers占用的时间。这三块时间加起来会接近2.9*8(P的个数);0.40 ms表示整个进程在markTermination阶段STW停顿时间(0.050 * 8)。
  • 4->4->2 MB 按顺序分成三部分,4表示开始mark阶段前的heap_live大小;4表示开始markTermination阶段前的heap_live大小;2表示被标记对象的大小。
  • 5 MB goal 表示下一次触发GC的内存占用阀值是5MB,等于2MB * 2,向上取整。
  • 12 P 使用的Processor数量

总结

以上就是在golang中垃圾回收的大致流程,总的来说使用三色标记法进行标记清除,并且标记时与程序运行并行,为了解决问题使用写屏障来记录标记过程中对象的变更。总来的来说也是为了提高垃圾回收的效率,并且尽可能的减少STW的时间。
了解下来,与java的分代回收相比,golang中的回收算法理解起来更加简单一些。

参考文章

https://studygolang.com/articles/21569
https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/
https://www.jianshu.com/p/8b0c0f7772da
http://legendtkl.com/2017/04/28/golang-gc/