垃圾回收算法
业界常见的垃圾回收算法有以下几种:
- 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。
- 优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阈值才返回。
- 缺点:不能很好的处理循环引用,而且实时维护引用计数,也有一定的代价。
- 代表语言:Python,PHP,Swift。
- 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收
- 优点:解决了引用计数的缺点。
- 缺点:需要STW,即要暂停掉程序运行来进行标记。
- 代表语言:Golang(三色标记法)
- 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
- 优点:回收性能好
- 缺点:算法复杂
- 代表语言:JAVA
golang垃圾回收
原理
垃圾回收的核心就是标记出哪些内存还在使用(被引用),哪些内存不再使用了(未被引用)。把未被引用的内存回收掉,以供后续继续使用。
如上图,垃圾回收就是从root对象开始扫描,把root对象引用的内存标记为“被引用”,考虑到内存块中存放的可能是指针,所以还需要递归进行标记,全部标记完成后,只保留被标记的内存,未被标记的全部标识为未分配即完成了回收。
内存标记
在内存分配原理中介绍了span数据结构,span中维护了一个个内存块,并由一个位图allocBits表示每个内存块的分配情况。在span数据结构中还有另外一个位图gcmarkBits用于标记内存块被引用的情况。
看上图,allocBits中记录每个块的分配情况,gcmarkBits中记录了每块内存标记情况。标记阶段对每块内存进行标记,有对象引用的内存标记为1,没有引用到的保持默认为0。
allocBits和gcmarkBits数据结构是完全一样的,标记结束就是内存回收,直接把allocBits指向gcmarkBits,那么标记过的就是存活的,就是已分配的块,gcmarkBits则会在下次标记的时候重新分配内存,非常巧妙。
三色标记法
这里的三色只是对应垃圾回收过程中对象的三种状态:
- 灰色:对象还在标记队列中等待。
- 黑色:对象已被标记,gcmarkBits对应的位为1(本次gc不会被清理)。
- 白色:对象未被标记,gcmarkBits对应的位为0(本次gc中被清理)。
现在就看一下gc的过程把,root变量是栈上分配的局部变量,刚开始内存中的对象状态如下:
初始的时候所有对象都是白色,然后开始扫描root对象a,b。由于a,b引用了A,B,所以A,B变为灰色对象。
接下来分析灰色对象,分析A时,A没有引用其他对象很快就转为了黑色,B引入了D,则B转入黑色的同时还要将D转为灰色,进行接下来的分析。

D转入黑色,并且D没有引用其他对象,所以没有新增的灰色对象,那么标记过程结束。

最终,黑色的对象会被保留下来,白色的对象会被回收掉。
stop the world
在垃圾回收的过程中,需要进行标记,而在标记的过程中需要保证内存没有发生变化,否则回收过程中指针传递引起内存引用关系变化,如果错误回收了还在使用的内存,结果将是灾难性的。
golang中的STW就是停掉所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复goroutine。
STW时间的长短直接影响了应用的执行,时间过长对于一些web应用来说是不可接受的,这也是广为诟病的原因之一。不过golang对于优化垃圾回收算法,缩短STW的时间,也很大改善了这种情况。
垃圾回收优化
写屏障
STW目的是为了防止GC扫描的时候内存变化而停掉goroutine,而写屏障就是让goroutine与GC同时运行的手段。 虽然写屏障不能完全消除STW,但是可以大大减少STW的时间。
写屏障在GC的特定时机开启,开启后指针传递会把指针标记,即本轮不回收,下次GC时再确定。
GC过程中新分配的内存会被立即标记用的并不是写屏障技术,也即GC过程中分配的内存不会在本轮GC中回收。
辅助GC
为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与这一部分GC的工作,即帮助GC做一部分工作,这个机制叫做Mutator Assist。
垃圾回收触发时机
内存分配量达到阈值触发GC
每次内存分配时都会检查当前内存分配量是否以达到阈值,如果达到阈值立即启动GC。
阈值=上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,也就是每当内存扩大一倍时启动GC。
定期触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中声明:
var forcegcperiod int64 = 2 * 60 * 1e9手动触发GC
使用runtime.GC()来手动触发GC。主要用于GC性能测试和统计。
GC性能优化
GC性能与对象的数量负相关,对象越多GC性能越差,对程序影响越大。所以一个优化思路就是减少对象分配个数,比如对象复用或使用大对象组合多个小对象。
此外,由于内存逃逸现象,还有隐式的内存分配也会产生,也可能称为GC的负担。