目录
摘自
概括
GC 只负责回收堆,不负责回收栈,栈中的内存在用完就被直接释放了。
常见的垃圾回收算法有三种:引用计数、标记&清除、分代收集。
Golang 垃圾回收——三色标记法,属于标记&清除的一种:
三色对应了垃圾回收过程中对象的三种状态:
【黑色】:已被标记为使用中,不是垃圾,不会在 GC 中被回收。
【灰色】:还在标记队列中等待,正在被判断是否是垃圾,等待判断完后去到白或黑。
【白色】:未被标记为使用中,是个垃圾,会在 GC 中被回收。
详细
首先,GC 不回收什么?
在我们程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),而 GC 不负责回收栈中的内存。那么这是为什么呢?
主要原因是栈是一块专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈。除此以外,栈中的数据都有一个特点——简单。比如局部变量就不能被函数外访问,所以这块内存用完就可以直接释放。正是因为这个特点,栈中的数据可以通过简单的编译器指令自动清理,也就不需要通过 GC 来回收了。
## 垃圾回收算法
目前比较常见的垃圾回收算法有三种:
【 引用计数 】:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数 -1,当对象引用计数为 0 时回收该对象。
代表语言:Python、PHP、Swift
优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。
缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。
【 标记-清除 】:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。
代表语言:Golang(三色标记法)
优点:解决了引用计数的缺点。
缺点:需要 STW(stop the world,即暂停所有协程运行)。
【 分代收集 】:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
代表语言:Java
优点:回收性能好
缺点:算法复杂
## Go垃圾回收——三色标记法
三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:
黑色:已被标记为使用中,不是垃圾,不会在 GC 中被回收。
灰色:还在标记队列中等待,正在被判断是否是垃圾,等待判断完后去到白或黑。
白色:未被标记为使用中,是个垃圾,会在 GC 中被回收。
具体流程如下图:
回收原理
通过上图,应该对三色标记法有了一个比较直观的了解,那么我们现在来讲讲原理。简单的讲,就是标记内存中那些还在使用中(即被引用了)的部分,而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收,以供后续内存分配使用。上图中的 A、B、D 就是被引用正在使用的内存,而C、F、E 曾经被使用过,但现在没有任何对象引用,就需要被回收掉。
而 Root 区域主要是程序运行到当前时刻的栈和全局数据区域,是实时正在使用到的内存,当然应该优先标记。而考虑到内存块中存放的可能是指针,所以还需要递归的进行标记,待全部标记完后,就会对未被标记的内存进行回收。
内存标记
golang 中采用 span 数据结构管理内存,span 中维护了一个个内存块,并由一个位图 allocBits 表示内存块的分配情况,而上文中提到的 gcmarkBits 是记录每块内存块被引用情况的。
如上图,allocBits 记录了每块内存的分配情况,而 gcmarkBits 记录了每块内存的标记情况。在标记阶段会对每块内存进行标记,有对象引用的内存标记为 1,没有对象引用的为 0。而 allocBits 和 gcmarkBits 的数据结构是完全一样的,在结束标记后,将 allocBits 指向 gcmarkBits,则有标记的才是存活的,这样就完成了内存回收。而 gcmarkBits 则会在下次标记时重新分配内存。
## 垃圾回收优化
在前文中提到,golang 的垃圾回收算法属于 标记-清除,是需要 STW 的。STW 就是 Stop The World 的意思,在 golang 中就是要停掉所有的 goroutine,专心进行垃圾回收,待垃圾回收结束后再恢复 goroutine。而 STW 时间的长短直接影响了应用的执行,如果时间过长,那将是灾难性的。为了缩短 STW 时间,golang 不对优化垃圾回收算法,其中写屏障(Write Barrier)和辅助GC(Mutator Assist)就是两种优化垃圾回收的方法。
【 写屏障(Write Barrier) 】:上面说到的 STW 的目的是防止 GC 扫描时内存变化引起的混乱,而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。
【 辅助 GC(Mutator Assist) 】:为了防止内存分配过快,在 GC 执行过程中,GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作。
## 垃圾回收触发机制
【 内存分配量达到阈值 】:每次内存分配都会检查当前内存分配量是否达到阈值,如果达到阈值则触发 GC。阈值 = 上次 GC 内存分配量 * 内存增长率,内存增长率由环境变量 GOGC 控制,默认为 100,即每当内存扩大一倍时启动 GC。
【 定时触发 GC 】:默认情况下,2分钟触发一次 GC,该间隔由 src/runtime/proc.go 中的 forcegcperiod 声明。
【 手动触发 GC 】:在代码中,可通过使用 runtime.GC() 手动触发 GC。
GC 优化建议
由上文可知,GC 性能是与对象数量有关的,对象越多 GC 性能越差,对程序的影响也越大。所以在开发中要尽量减少对象分配个数,采用对象复用、将小对象组合成大对象或采用小数据类型(如使用 int8 代替 int)等。