Go的GC
内存布局
首先,要先搞清楚垃圾回收
回收的是什么?这要从进程的内存布局讲起。
上图是linux中的进程内存布局,其中:
- Stack是栈内存,存放函数出入参以及局部变量
- Head是堆内存,和数据结构中的堆没有关系,用于存放动态创建的对象
- Bss是静态内存分配,用于存放程序中未初始化的全局变量
- Data用于存放一些常量数据,程序中已经初始化的全局变量就存放在这
- Test用于存放程序代码
进程的栈内存是为函数准备的,函数调用完了之后就被释放,不需要程序GC,而堆内存是程序运行过程中动态申请的空间用于存放动态创建的对象,这个才是GC的对象。
GC的类型
各种语言GC的类型有2种:
- 手动GC,比如C/C++
- 自动GC,比如PHP、GO
手动GC的优点是可以由开发者控制内存的分配和销毁,GC更精确,不需要系统处理,效率高,但是由于程序的复杂性,容易出现指针悬挂
和内存泄露
的问题
自动GC则简化了开发者对内存的操作,可以由系统更好地管理内存,可以进行预分配,使用内存吃,不会产生内存碎片,但缺点也很明显,增加了系统的负担,降低系统的性能
自动GC的方案
主流的自动GC方案有2种:
- 引用计数
- 标记删除
PHP采用的就是引用计数的方案,而GO则采用了标记删除的方案
Go的GC
Go的标记删除GC方案称为三色标记法
,具体步骤是:
- 所有的对象初始都是白色
- 从根对象开始,将找到的根对象都标记为灰色
- 遍历灰色对象,将灰色对象引用的对象标记为灰色,将自身标记为黑色
- 继续第3步,直到所有可达的对象都标记为黑色
- 白色的对象即为不可达对象,被视为垃圾,是回收的对象
由于GC和代码逻辑是并行的,在GC过程中,代码逻辑会破坏当前标记好的引用关系,比如当前某条引用路径是黑-》灰-〉白,代码将灰色对象对白色对象的引用赋值给了黑色对象,导致灰色与白色的引用断开,黑色对象直接引用白色对象,而按照三色标记法
,是不会直接检测黑色对象的引用的,导致无法找到这个白色对象,最终当作垃圾处理
所以Go的GC有一个在GC过程中暂停所有程序逻辑运行的机制:STW(stop the world),但是这样会导致GC的时候系统出现性能波动。
我们来分析下,不采用STW的会导致对象丢失的两个条件:
- 白色对象被黑色对象引用
- 灰色对象与白色对象的引用关系被破坏
如果可以解决这两个问题,则可以不需要STW,有两个方案可以采用:
- 强不变式:不允许黑色对象引用白色对象
- 弱不变式:黑色对象可以引用白色对象,但是白色对象必须直接或间接被灰色对象引用
GO语言对上述方案的两种实现:
插入写屏障
将一个对象引用另一个对象时,将另一个对象标记为灰色
删除写屏障
当一个白色对象被另一个对象解除引用解除时,被引用对象被标记为黑色
GO使用了两者混合的模式:
- GC刚开始的时候,会将栈上的可达对象全部标记为黑色。
- GC期间,任何在栈上新创建的对象,均为黑色。将栈上的可达对象全部标黑,最后无需对栈进行STW,就可以保证栈上的对象不会丢失
- 堆上被删除的对象标记为灰色
- 堆上新添加的对象标记为灰色
Go 的GC优化历程
- go 1.3 之前采用标记清除法,需要STW
- go 1.5 采用三色标记法,插入写屏障机制(只在堆内存中生效),最后仍需对栈内存进行STW
- go 1.8 采用混合写屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率