go 垃圾回收
文章目录
历史
v1.0 标记清除 & STW
v1.1 并行标记清除 & STW
v1.5 三色标记清扫的并发垃圾收集器,写屏障技术
v1.8 使用混合写屏障
v1.9 彻底移除暂定程序的重新扫描栈的过程
之后就没啥大改变了
原理
标记清除(mark-sweep)
过程:从跟对象开始,标记能达到的所有对象,全部标记完成后开始清除,将没有标记的对象清除。整个过程会stop the world。
问题: 最早的go就是用的这个,现象是程序会感觉到明显的卡顿一会,这是因为整个过程会STW,这时候用户程序不会执行。
三色标记算法
屏障技术
两种三色不变性:
弱三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径;
强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;
插入写屏障
Dijkstra 的 插入写屏障
可能会错误标记需要被回收的对象,该对象在下次gc回收
保证了强三色不变性
缺点:因为栈上对象不能开启写屏障,所以需要需要在标记完成后STW重新扫描一遍栈对象。
问题:为啥不能在栈上开启写屏障?因为栈上的写入指针额外开销比较大?为什么?
删除写屏障
Yuasa 的 删除写屏障
可能会错误标记需要被回收的对象,该对象在下次gc回收
保证了弱三色不变性
缺点:开始前需要将全栈扫黑,栈对象指向的所有一级对象都置灰,让堆中的对象在灰色保护下。这个需要STW。
混合写屏障
同时使用以上插入写屏障和删除写屏障;
其实就是用的删除写屏障,但是删除写屏障开始前要保证所有的堆对象在灰色保护下,这样需要把栈对象扫黑,栈对象指向的一级对象扫灰,这个过程是需要STW的,现在因为这个操作比较耗时要避免这个STW过程,所以go不暂停整个程序,而是一个goroutine一个goroutine的暂停执行这个操作,因此引入的插入写屏障。
增量和并发
增量垃圾收集
就是用户程序和gc交替进行,但是gc每次都执行一部分,不全部执行,将一个长时间的gc分割成几个小段的gc,这样就避免了整个gc造成长时间的STW。
并发垃圾收集
就是用户程序和gc同时进行。
实现
开始GC
触发GC时机
-
后台定时检查
-
用户手动触发
-
申请内存时根据堆大小触发
清理阶段(Sweep)
清理触发时机
- 在heap分配内存的时候;
- mcache从mcentral申请mspan的时候;
- 各种情况下触发
sweepone
触发;
下面分别介绍:
1. heap 分配内存的时候触发清理
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) (*mspan, bool) {
var s *mspan
systemstack(func() {
if !isSweepDone() {
//如果gc清理没有结束,那么清理npages的内存页
h.reclaim(npages)
}
s = h.allocSpan(npages, spanAllocHeap, spanclass)
})
……
return s, isZeroed
}
2. mcache申请mspan
说下mcache从mcantral上申请mspan的流程:
- 先从 mcentral 的 partial 的 swept 拿
- 1 拿不到再从 mcentral 的 partial 的 unswept 拿
- 2 拿不到再从 mcentral 的 full 的unswept 拿
- 3 拿不到再从 mheap 拿
其中设计到清理的是 2,3和4(4就是上面说的从heap申请内存的时候触发的)代码如下:
func (c *mcentral) cacheSpan() *mspan {
//1. 先从mcentral的 partial 的 swept链表找
if s = c.partialSwept(sg).pop(); s != nil {
goto havespan
}
//2. 从mcentral的 partial 的unswept 上找,找到先sweep,后用
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
s.sweep(true) //传true代表不释放该span
sl.dispose()
goto havespan
}
}
//3. 从mcentral 的 full 中的unswept中找,找到先sweep,后用
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop()
if s == nil {
break
}
if s, ok := sl.tryAcquire(s); ok {
// We got ownership of the span, so let's sweep it.
s.sweep(true)
// Check if there's any free space.
freeIndex := s.nextFreeIndex()
if freeIndex != s.nelems {
s.freeindex = freeIndex
sl.dispose()
goto havespan
}
// Add it to the swept list, because sweeping didn't give us any free space.
c.fullSwept(sg).push(s.mspan)
}
// See comment for partial unswept spans.
}
// 4. 从 mheap 上拿
s = c.grow()
if s == nil {
return nil
}
}
3. 其他触发 sweepone() 函数处理
- gc开始的时候,会清理上一轮gc的垃圾对象。在 gcStart 函数中调用;
- 在gc扫描完之后,gcMarkTermination函数调用gcSweep函数调用sweep清理;(一般走到这里,这里是不允许并发的清理的时候,同步清理,这里需要STW,看这个逻辑一直走不进去的,_ConcurrentSweep一直是true,就进不去这个逻辑)
- 异步程序 bgsweep 函数中一直调用sweep函数。bgsweep是runtime.main中开的goroutine;
清理
清理使用的是 runtime.sweepLocked.sweep
函数,用来清理一个span,其中主要做的事情是:
- 设置该mspan的状态
- 将mspan的allocCount设置成gc标记的对象数量;
- 将mspan的freeindex设置成0,freeindex代表的是内存分配的位置;
- allocBits = gcmarkBits; gcmarkBits 重建。
- 如果是小对象<32kb,spanclass==0
- 如果mspan中没有被分配出去的对象了,直接将该mspan放回mheap中;
- 如果mspan中有被分配的对象,并且都被分配出去了,将该mspan放进mcantrel的full swept集合中;
- 如果mspan中有被分配的对象,并且没有全部被分配出去,将该mspan房间mcantrel的partial swept集合中;
- 如果是大对象>=32kb, spanclass != 0,直接放进mcentral的full swept 集合中;
其他
GC状态流程
-
一开始在 _GCoff 状态
-
在gcStart函数中将状态设置成_GCmark
-
在gcMarkTermination函数中,短暂的将状态设置成_GCmarktermination,之后将状态设置成 _GCoff。
-
额外:写屏障就是跟局这个状态来的。只有_GCoff的时候是关闭的,其他是开启的。
STW流程
- gcStart的时候STW一会,扫跟对象,开启写屏障,开启辅助扫描
- gcMarkDone的时候STW一会,关写屏障,关辅助扫描(这里会将所有的p上面的mcache的mspan扫描一下)
在markTermination阶段关闭写屏障,会不会误删关闭屏障后新建的对象?
不会,分两种情况。
- mspan没有被mcache缓存,那么在重新分配对象的时候一定是重新拿到了mcache上,这时候mspan.sweepgen = mheap.sweepgen。 这时候不会被清理,因为只会清理比mheap.sweepgen小的。
- mspan已经被mcache缓存,那么在重新分配对象的时候,因为关闭了写屏障,那么mcache在满之后被uncacheSpan的时候,岂不是被清理掉了?这个也不会,因为STW的时候关闭写屏障,然后马上就遍历了所有的p的mcache,将所有的mcache清理一遍,因为这时候是STW的,所以不会有新对象产生,在重新开启world的时候,能保证所有的p上面的mcache都是没有mspan的。(只要保证关闭写屏障很清理p的mcache是在STW过程中就没问题,清理p的mcache是在startTheWorldWithSema[->procresize->destroy->freemcache->releaseAll->uncacheSpan]函数中处理的)
清理中的 sweepgen 字段
在mheap上和每个mspan上都有一个该字段。
mspan上面的定义:
//sweepgen是当前mspan上的,h是heap
// if sweepgen == h->sweepgen - 2, 需要清理
// if sweepgen == h->sweepgen - 1, 正在清理
// if sweepgen == h->sweepgen 已经清理完了并且可以复用
// if sweepgen == h->sweepgen + 1,该span的在sweep开始前就被cache了,目前仍然在mcache中,需要被清理
// if sweepgen == h->sweepgen + 3,该span被mcache获取了,并且目前仍然在mcache上。
// 在每个gc周期,h->sweepgen都增加2
mspan的sweepgen流程:
- 在mheap分配一个mspan的时候,其sweepgen = h.sweepgen;
- 一个mcentral上面的mspan被分到mcache上后,被认为是已经cache的,该mspan的sweepgen+=3;
- mcache将mspan放回mcentral的时候,如果当前sweepgen=h.sweepgen+3会将该mspan的sweepgen设置成mheap的sweepgen;
- mcache将mspan放回mcentral的时候,如果当前sweepgen=h.sweepgen+1会将该mspan的sweepgen设置成mheap的sweepgen-1,然后直接调用sweep清理;
- 在sweep之前tryAcquire的时候,会将mspan设置成h.sweepgen-1,代表正在清理
- Sweep 的时候,会将mspan的sweepgen设置成mheap的sweepgen;
mheap的sweepgen流程:
- 调用gcSweep的时候,将其+=2
- 没了……
这么看mcentral的[2]full,这个数组,每一代都翻转一下子,一个代表swept,一个代表unswept的。