https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
有两种常见的自动管理堆内存的方法:
- 引用计数/智能指针
- 追踪式垃圾回收(对堆内存的对象关系图进行可达性分析)
- 根对象: 包括所有栈上对象,全局变量
典型的STW(stop the world)算法,当进行垃圾回收时,先暂停用户程序,然后从根对象出发对堆对象进行可达性标记(比如bfs/dfs),标记完后遍历所有的堆对象,回收掉不可达对象
这是一种并发算法,不需要或只需要短暂的暂停用户程序
定义三色: 黑白灰
- 灰色: 可达对象
- 黑色: 由灰色衍生出来的可达对象
- 白色: 不可达对象
算法流程:
首先所有根对象置为灰色
- 从灰色对象中选择一个,置为黑色对象
- 将黑色对象指向的所有对象置为灰色
- 重复1,2,直到不存在灰色对象
- 回收剩余的白色对象
由于在标记过程中,对象图可能改变,所以需要作如下操作:
- 修改指针之前,必须先对被指向的对象标记为灰色,由于现代cpu的乱序执行和多发射,这需要我们用写内存屏障来实现
-
插入写屏障
-
writePointer(slot, ptr): shade(ptr) //染灰 *slot = ptr
-
-
删除写屏障
-
writePointer(slot, ptr) shade(*slot) //染灰 *slot = ptr
-
-
混合写屏障(v1.8引入)
-
writePointer(slot, ptr): shade(*slot) if current stack is grey: shade(ptr) *slot = ptr
-
注意: 不会在所有的根对象上开启写屏障,因为一个程序可能由成百上千个goroutine,如果在所有的goroutine的栈上开启写屏障,压力太大
看了了b站刘丹冰的视频后,有了更深的理解
go的gc算法:
v1.3 StopTheWorld 标记清扫法 stw->mark->sweep->stw
优化: stw->mark->stw->sweep(串行标记,并发清扫)
v1.5 三色标记法 这里的mark是分层的bfs.
这个方法可以解决并发吗?
对于新创建的对象:
对于已有对象的并发标记问题:
如果一个白色对象原来被正常引用(或是作为白色新创建节点,被黑色对象引用), 在mark过程中变为仅被黑色对象引用,显然就会最终仍是白色,被丢失,这是因为三色标记法是基于灰色对象标记下一个灰色对象的,不会对黑色对象所引用的对象标记
解决方法:
强三色不变性: 禁止mark过程中的黑色对象指向白色对象
弱三色不变性: 一个白色对象的上游链路必须存在灰色对象,此时其可被黑色对象引用(只要上游灰色存在,最终一定会标记到自己)
如何实现:
屏障技术(实际上,就是在变更引用关系的时候触发回调函数)
初始时 A.field1 = B
变更: A.field1 = C
则: B触发删除写屏障,C触发插入写屏障(要么开启删除写屏障,要么开启插入写屏障)
最后v1.8 优化为混合写屏障
插入写屏障:
被黑色节点引用的对象置灰色
1. 堆空间作为根节点时,白色对象若被黑色对象引用,触发回调,自己置为灰色
2. 栈空间不触发插入屏障,这是为了保证速度. 所以为了保证新节点不丢失,要最后stw扫描栈,重新扫描mark一次
因此:
插入写屏障的不足: 结束时stw扫描栈,10-100ms
删除写屏障:
被删除的对象直接置为灰色
显然,如果这个对象确实是不再被引用,而不是变更为被黑色对象引用,那么这个对象就会在本轮不被删除,但是无论如何,下轮gc仍然删除.所以缺点就是有可能造成延迟删除.但是这不可避免,因为你无法判断他是变更引用关系到黑色对象上,还是真的删了,只能先妥协一轮.
v1.8混合写屏障:
- 栈上对象(根节点为栈)全部置为黑色,后续被栈引用的新对象均置为黑色(防止重复stw扫描栈)
- 栈不开启写屏障
- 堆上被删除的对象置为灰色
- 堆上被插入的对象置为灰色
满足弱三色不变性
注意这里,栈对象也是分配在堆上的,因为go程是用户态的,详见GMP