Golang Runtime是Go语言运行所需要的基础设施,主要有以下功能:

  1. 协程调度,内存分配,GC
  2. 操作系统及CPU相关的操作的封装 (信号处理、系统调用、寄存器操作、原子操作等),CGO
  3. pprof,trace,race检测的支持
  4. map,channel,string等内置类型及反射的实现

    垃圾回收 GC

    垃圾回收是一种自动内存管理的机制,当程序向操作系统申请的内存使用完毕后,来及回收机制将其回收并供其他代码进行内存申请时复用,或将其归还给操作系统。
    GC的作用提现在两个方面:
  • 程序员可以更多地关注代码本身,而无需对内存进行手动的申请和释放操作
  • GC仅在程序需要进行特殊优化的时候才会现身,是一种自动触发机制

    Golang的垃圾回收机制

    Go语言的GC目前使用的是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法。Go语言的GC发展历程如下:
版本 发布时间 GC STW时间
v1.1 2013/5 STW 百ms-几百ms级别
v1.3 2014/6 Mark STW,Sweep并行 百ms级别
v1.5 2015/8 三色标记法,并发标记清除 10ms级别
v1.6 使用bitmap来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存 10ms内
v1.7 2ms内
v1.8 2017/2 hybird write barrier,混合写屏障 0.5ms左右
v1.9 彻底移除了栈的重复扫描过程
v1.12 整合两个阶段的Mart Termination
v1.13 着手解决向操作系统归还内存的问题,提出了新的Scavenger
v1.14 全新的页分配器替代Scavenger,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的STW时间过长问题。

三色标记法

三色标记法是对标记-清扫算法的改进,主要是针对标记阶段进行改进:

  1. 起初所有对象都是白色
  2. 从根节点出发扫描所有可达对象,标记为灰色,放入待处理队列
  3. 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色
  4. 重复3,直到灰色队列为空,此时白色对象即为垃圾,进行回收

Animation_of_tri-color_garbage_collection.gif

并发标记清除

三色标记法在扫描之前要执行STW (Stop The World)在扫描完成后要进行STW (Start The World),即在扫描的过程中,所有线程要被暂时冻结,保证被扫描的对象状态不发生改变,STW时间过长的话,会影响GC性能。
Golang的GC和用户代码是并发执行的,这就带来了一个问题,由于用户代码的执行,可能会使得在GC扫描和标记两个阶段之间,某个结点的引用状态发生了改变,很可能将一个实际存活的节点被标记为白色而被误清除,或者某个已经死亡的节点被误标记为存活状态而造成内存泄漏,为了解决这个问题,Go语言引入了写屏障的概念

写屏障、混合写屏障

  • 写屏障

写屏障的主要功能就是:在每一轮GC开始时初始化一个“写屏障”,用于保存第一次扫描时各对象的状态,再下一次扫描时,各个对象的状态与上一次扫描的状态进行对比,将引用状态变化的对象标记为灰色,以防其丢失,然后继续处理剩下的对象。写屏障的引入,是为了保证各对象的三色不变性。

  • 混合写屏障

为了保证三色状态稳定性,主要是需要避免如下两个情况的出现

  1. 避免黑色对象引用白色对象 —— 使用Dijkstra插入屏障解决
  2. 避免灰色对象到达白色对象的路引用路径被破坏 —— 使用Yuasa删除屏障解决

Go语言1.8的时候为了简化GC流程,同时减少标记终止阶段的重扫成本,将Dijkstra插入屏障和Yuasa删除屏障进行混合,形成了混合写屏障。
混合写屏障的基本思想是:对正在被覆盖着色的队形进行着色,且如果当前栈未扫描完成,则同样对指针进行着色。

Go语言中的GC流程

当前版本的Go语言以STW为界限,将GC分为五个阶段:

  1. 清扫终止阶段 Sweep Termination

为下一个阶段的并发标记做准备,启动写屏障

  1. 扫描标记阶段 Mark

与赋值器并发执行,写屏障开启

  1. 标记终止阶段 Mark Termination

保证一个周期内标记任务完成,停止写屏障

  1. 内存清扫阶段 GCoff

将需要回收的内存归还到堆中,写屏障关闭

  1. 内存归还阶段 GCoff

将过多的内存归还给操作系统,写屏障关闭
gc-process.png

有了GC为什么还会出现内存泄漏?

这里的内存泄漏是指:预期的能很快被释放的内存,由于附着在了长期存活的内存上,或生命周期意外地被延长,导致预计能够立即回收的内存长时间得不到回收。
可能出现的情况如下:

内存未按预期得到快速释放

某个变量不经意间被附着在一个全局对象上

goroutine泄漏

goroutine上会需要消耗一定的内存空间来保存上下文信息以及一些变量信息,当一个goroutine长时间不被结束时,可能造成内存泄漏:

  1. func keepalloc() {
  2. for i:=0;i<10000;i++{
  3. go func(){
  4. select{}
  5. }
  6. }
  7. }

参考

Go Runtime浅析
GC的认识
Golang 垃圾回收剖析
搞懂Go垃圾回收