目录
GO源代碼中GC的位置在go/src/runtime/mgc.go
概述
GO语言中垃圾回收进程和正常程序是并行的,即在程序运行的过程中不断观察可以回收的内存对象。
可清除的对象:绝对不可达的对象(作为一个分配的对象,后续程序无法再使用它,即不可达)
判断不可达的方法:
在 GC 领域里,判断对象存活的主流思路是两个,「引用计数」和「可达性分析」。
1 引用计数
顾名思义,引用计数的思路就是给每个对象进行计数,每被其它对象引用一次,计数就 +1,引用失效后,计数就 -1。当计数器的数值为 0,就意味着它没有被使用,可以回收。
引用计数有一个特别严重的问题:无法处理循环引用。
如果两个对象互相引用,导致引用计数一直为都为1而无法清除两个对象。
2 可达性分析
可达性分析的思路就是通过引用链路判断对象是否可被触达,如果能触达说明该对象当前正在被使用,不可回收;反之,没有触达到的对象则认为是无使用的,可以回收。
兩步回收
具体实现是 Mark and Sweep:
Mark:对于每一个正在执行的 Goroutine,GC 会扫描其 stack 获得所有的 pointer 变量(以及 pointer 的 pointer 等),并在内存中 mark 这些 pointer 对应的内容。
Sweep:扫描整片内存并回收没有被 mark 的部分。
STW:程序暂停
GO在1.3版本中是(Mark加STW)和Sweep,意味着标记对象时需要暂停所有程序的执行防止垃圾回收进程和程序执行出现冲突,但后续改进之后不再需要STW。
GC采用的垃圾回收方法是三色标记法,属于可达性分析的范围。
三色标记法:
这里介绍最开始的方法,即标记的时候程序是暂停的。
白色对象:默认值。本次回收没被扫描过的对象都是白色的。确认不可达的对象也是白色,但是会被标记「不可达」。
灰色对象:中间状态。本对象有被外部引用,但是本对象引用的其它对象尚未全部检测完。
黑色对象:本对象有被其它对象引用,且已检测完本对象引用的其它对象。
而GC程序会不断地扫描灰色和白色的对象,直到只剩下两种颜色的对象,白色和黑色对象,扫描结束,可以对标记不可达的白色对象进行清除。
这个过程我们可以用链表来表示,创建两个带头节点的链表,头节点一般都是可达的,但你也可以把他弄成不可达的。。。
最开始对象都是白色的。因为还没扫描。
![](https://img-blog.csdnimg.cn/40bb2cadfaeb4e7ba5687589f9be0116.png)
接下来怎么搞?找到所有直接可达的对象,例如此时的头节点1和11,把这两个变成灰色的,
![](https://img-blog.csdnimg.cn/9b1f8d4137f042b698fd2baa67e51f48.png)
接下来灰色的变成黑色的,灰色指向的白色的变成灰色的。
![](https://img-blog.csdnimg.cn/eee0a73d1e21494dbdaceaf9a1f3171c.png)
黑色对象不变。灰色对象变成黑色的,指向的白色对象变成灰色的,最后,所有对象移入黑色,即所有对象都可达,不用回收,
![](https://img-blog.csdnimg.cn/5acc58a2049044658793079912a48b9f.png)
不用想,如果一个节点没有被其他节点引用,则永远不会变成一个灰色对象,意味着也不会变成黑色对象,因此不会被保留。这其实就是GO在1.3版本时的实现方式了,但需要程序停住等着垃圾回收。后续会变成并行版本。
我们想想什么情况下会有错误,当然只有回收和赋值并行会导致错误。想不到下面会有。
那么GO的垃圾回收进程(现在的版本)如何进行具体的工作呢?
环境变量GOGC设置最初的垃圾收集目标百分比。当新申请的数据和前次垃圾收集剩下的存活数据的比率达到该百分比时,就会触发垃圾收集,例如我们现在正在使用4M,我们将在达到8M时进行GC。默认GOGC=100。设置GOGC=off 会完全关闭垃圾收集。runtime/debug包的SetGCPercent函数允许在运行时修改该百分比。但上面的这个触发条件在1.5后已经修改成动态的了,因为GC和程序是并行的,在GC触发后仍然会分配内存,要想在GC完成后到达目标的GOGC,触发在8M不是最好的点,可能是4-8之间的一个点。(涉及到调步算法,这个先放在这里)
// src/runtime/debug
func SetGCPercent(percent int) int
func SetMaxStack(bytes int) int
代码部分包含五个阶段,每个时代依次运行五个阶段的代码。
垃圾回收的正确性体现在两点:
1、不应出现对象的丢失。
2、不应错误的回收还不需要回收的对象。
而对于三色法来说破坏这两点的唯一方法可以显式的描述成以下两个条件(两个需要同时满足):
条件 1: 赋值器(这里就是赋值操作,一个名词不需要特殊理解)修改对象图,导致某一黑色对象引用白色对象,此时虽然对象2不需要回收,但是因为GC不会再扫描1,那么也就无法把2放到灰色区域,2在这个阶段会被回收,出现错误。(其实就是在某个对象已经在三色图里变成黑色时,让他引用一个新对象,这个对象会是白色,当然,只有GC和程序并行才会出现这种情况)
![](https://img-blog.csdnimg.cn/20d0dd3860f2410985892d8f22981b00.png)
一个条件不会锁死,例如这种情况,虽然黑色直接指向白色,但是还有一个灰色的指向白色,3最后仍热可以被安全的放到黑色区域中。
![](https://img-blog.csdnimg.cn/eb725181bb934a888b9e32014058d13a.png)
条件 2: 从灰色对象出发,到达白色对象的、未经访问过的路径被赋值器破坏。
但如果二也满足就不太行了,
那就是把这个路径干掉,但其实原来的图5只有黑色指向白色和这个也是一样的,3对象又变成孤儿了。
![](https://img-blog.csdnimg.cn/e88215a3ed22443c8542de57f234c111.png)
![串行](https://img-blog.csdnimg.cn/65d4751219e64c1bafd55983cdbfab16.png)
![](https://img-blog.csdnimg.cn/1b24e9db9ea14763b4b41c6bcb7c16cb.png)
因为传统的Mark and Sweep方法需要程序暂停,然后扫描所有的变量来实现垃圾回收,这就是串行,因此不会出现上述的问题,?此时意味着要对新创建的对象和删除对象做一些特殊处理,因为这那怎么做到回收进而赋值器和回收器程序并行执行呢两种操作可能导致并行时出现上面的错误。有几种解决办法
1、将白色对象作色成灰色
2、扫描对象并将其着色为黑色
3、将黑色对象回退到灰色
对于GO来说,具体实现为:
1.将对象分为堆上的对象和栈上的对象。
2.GC 开始将栈上的对象全部扫描并标记为黑色,无需 STW。并且之后不再进行第二次重复扫描。
3.在 GC 期间,任何在栈上创建的新对象,均为黑色。
4.在 GC 期间,在堆上被删除或者添加的对象都标记为灰色。后续继续扫描。
垃圾回收机制的写屏障表现形式就是赋值器写屏障,对于GO来说,采用的混合写屏障技术包含两部分:灰色赋值器的 Dijkstra 插入屏障与黑色赋值器的 Yuasa 删除屏障。
灰色赋值器的 Dijkstra 插入屏障
其实只要满足一点就可以避免出现错误,那就是新分配的对象不是连结在黑色旧对象上的白色对象,也就是条件一那样。那怎么办呢?于是把赋值器对已存活的对象集合的插入行为通知给回收器,进而产生可能需要额外(重新)扫描的对象。 如果某一对象的引用被插入到已经被标记为黑色的对象中,这类屏障会保守地将其作为非白色存活对象, 以满足强三色不变性。就是这样,新分配的对象我不管是被谁引用,我就把他弄成灰色的,一定没事。
func DijkstraWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
shade(ptr) //变成灰色
*slot = ptr
}
黑色赋值器的 Yuasa 删除屏障
其思想是当赋值器从灰色或白色对象中删除白色指针时,通过写屏障将这一行为通知给并发执行的回收器。 这一过程很像是在操纵对象图之前对图进行了一次快照。
具体操作则为把黑色对象降级为灰色对象,防止在添加新的连接后产生黑色对象直接指向白色对象的情况。
// 黑色赋值器 Yuasa 屏障
func YuasaWritePointer(slot *unsafe.Pointer, ptr unsafe.Pointer) {
shade(*slot) //对覆盖对象进行着色,变成灰色,使得无论何种情况都保证时灰色到灰色对象,或者白色到灰色对象
*slot = ptr
}
混合写屏障
同时兼顾了两种屏障,
func HybridWritePointerSimple(slot *unsafe.Pointer, ptr unsafe.Pointer) {
shade(*slot)
shade(ptr)
*slot = ptr
}