课程名称:深入Go底层原理,重写Redis中间件实战
课程章节:9-7,9-8
课程讲师:Moody
课程内容:
★标记-清除
对需要回收的内存先进行标记,后面GC会对其进行回收,对于类似于java这样的语言来说,这样的标记清除会产生很多碎片,但是go用的是TCMalloc模型,内存是提前按mSpanClass分配好的,所以不存在碎片的问题,因此go选用的是这种方式,但是go是对有用的进行标记,也就是三色法中的黑色\灰色对象
★标记-整理
对要回收的内存先进行标记,GC不仅仅会回收,还会整理整个内存块,把碎片给整理掉,但是整理的过程开销非常大,甚至可能造成整个服务的暂停,在过去并发量不高的年代,这种模式问题不大,随着高并发时代来临,GC速度远远赶不上增长规模,整理一次会耗费大量的资源,因此go没有选用该模式
★标记-复制
对要回收的内存先进行标记,GC会开辟一个新的内存块,把没标记的,依然在使用的内存整体复制搬迁到新的内存块里面,遗弃掉旧的内存块,用空间来换取时间。
★三色标记法
黑色对象:有用,已经分析扫描(有没有引用其他,有没有被其他引用)
灰色对象:有用,还未分析扫描
白色对象:无用,暂时没扫描到或者扫描到了就是没用
先从root set出发,进行深度优先搜索DFS,找到有用的对象,并标记为灰色
再对灰色对象进行分析扫描,先把该对象标记为黑色,对黑色对象指向的所有对象都标记为灰色,保证该对象和被该对象引用的对象都不会被回收,不停的通过DFS往下遍历,直到所有的灰色都被处理
其实 mspan 中使用 gcmarkBits 位图代表是否被垃圾回收扫描的状态,只有黑色和白色,mbits.setMarked() 设置的就是 gcmarkBits 对应的 index 位为 1。灰色是抽象出来的中间状态,没有专门的标灰的逻辑,放入到 gcw 中就是标灰。greyobject() 做的事情就是把自身 位置 标成黑色,代表它存活。最后把当前 位置 保存的 对象 放入到灰色集合,是为了扫描这个对象后续的引用。这里 位置 和 对象 的关系有点绕,需要细品。
剩余没有被标记的对象都是白色,白色对象会被GC清理掉
GC清理完成后,会暂停一段时间,当新一轮GC开始的时候,所有的对象再次恢复到白色状态,并被重新遍历和标记
★读写屏障
在并发标记内存的时候,为了保证正确性,避免出现不该被回收的内存被回收了(悬挂指针问题),要达成两种三色不变性
强三色不变性----黑色对象不会被指向白色对象,只会指向灰色或者黑色对象
弱三色不变性----黑色对象指向的白色对象必须包含一条从灰色对象经由白色对象的可达路径
▲插入写屏障
举例:如果从root出发,A里面引用了B,B又引用了C。此时,A放弃了对B的引用,而直接引用C,不再经由B。
GC从跟对象指向A的对象标记为黑色,并将A指向的B标记为灰色,因为B还是灰色,C没有被扫描,那么C是白色的
修改A对象指针,将原本指向B的指针改为指向C,C相当于新插入到A,这时候,触发写屏障,将C对象也变为灰色
DFS扫描,继续执行他的标记工作
此刻有:
满足强三色不变性。实际上在本次GC时候,B对象已经被A给放弃了,没有其他对象引用B,B实际上是不存活的对象,但是他最后也没有被回收,因为是灰色状态。而如果在本次GC中,A对象再次把指针从C改回B,C已经被标灰,也不会变白的。
▲删除写屏障
举例:从Root触发,A引用B,B引用C,C引用D
GC从root触发,把A标记为黑色,A由于引用了B,B也被标记为灰色
此刻,A对象删除了对B的引用,并引用了C,删除对B的引用的时候,触发删除屏障,但是B已经是灰色,在本次GC过程中不会被回收
A对象引用C的时候,C还是白色,在A引用C的时候,GC扫描已经过去了,不会重新扫描A引用的C,所以C依然是白色。这时候,用户程序把B对C的引用也给删了,触发删除屏障,白色的C被涂成灰色,灰色对象还会被放入一个专门的待扫描的池子里,等待GC在本次扫描中依次遍历灰色,直到发现C,发现C后执行正常的DFS扫描发现D,最终D也被标记为灰色
▲混合屏障
简单说上面两种屏障,就是被删除的对象不管有用没用,在本次GC中,只要删了就标记为灰色,防止误删;同样的,被添加的堆对象直接被标记为灰色,防止误删。
课程收获:
本节课主要学习了经典的go三色标记法,同时对同步GC产生的问题有了一定了解,并对解决方案进行了学习