go 混合写屏障:
- GC开始时,STW,对栈进行扫描,将所有可达对象置黑
- GC期间,任何栈上新创建的对象,皆置黑【1和2配合,可避免插入屏障对栈区的二次三色标记】
- 被删除的对象置灰
- 新挂载的对象置灰
疑问:栈上的对象如果是随着栈回收而被回收的话,为啥混合写屏障还会有对栈上内存的规则?
答案:(疑似)
Go语言中实际的栈帧布局如下所示(源代码中的注释):
image.png image.png
运行时可以计算出当前栈帧的函数参数、函数本地变量、寄存器信息SP、BP等一系列信息。
对每一个栈帧函数中的参数和局部变量,都需要对其进行扫描,扫描该对象是否仍然在使用。如果在使用,需要扫描bytedata位图判断对象中是否包含指针,如果包含指针则需要进行标记。其中函数执行到某一位置时,与某个参数和局部变量对应的位图bytedata是借助于编译时计算出来的。
func scanframeworker(frame *stkframe, state *stackScanState, gcw *gcWork) {
// 扫描局部变量
if locals.n > 0 {
size := uintptr(locals.n) * sys.PtrSize
scanblock(frame.varp-size, size, locals.bytedata, gcw, state)
}
// 扫描函数参数
if args.n > 0 {
scanblock(frame.argp, uintptr(args.n)*sys.PtrSize, args.bytedata, gcw, state)
}
}
什么情况下对象可能没有在使用了呢?例如如下所示,当foo()函数执行到调用bar() 函数时,局部对象t就已经没有被使用了,所以即便对象t中有指针,位图bytedata中全为0,代表参数不再被使用。一个不再被使用的对象,可以被回收,不需要再进行扫描。
func foo(){
t := T{}
t.a = 2
bar()
}
栈对象(stack object)
在Go语言早期就是通过上述方式对协程栈中的对象进行扫描的。但是这种方法在有些情况下会出现问题,例如在如下函数中, 对象t首先被p所引用,但是在之后的程序中,变量p的值发生了变化,这意味着,t其实并没有使用了。但是编译器由于难以知道P在何时会重新赋值导致t不再被引用,因此,编译器会采取保守的策略认为t对象仍然存在,从而,如果对象t中有指针指向了堆内存,就造成了内存泄露问题。因为这部分内存本应该被释放。
t := T{...}
p := &t
for {
if … {
p = …
}
}
为了解决内存泄露的问题,Go语言引进了 栈对象(stack object) 的概念。栈对象是在栈上能够被寻址的对象。例如上例中的t,由于其能够被&t的形式寻址,其一定在栈上有地址。所以t就被叫做 栈对象。因为并不是所有的变量都会存储在栈上,例如存储在寄存器中的变量就是不能被寻址的。
首先,编译器会在编译时将所有的栈对象记录下来,在垃圾回收期间,所有的栈对象会存储到一颗二叉搜索树中。接着,第二步将栈中所有可能指向栈对象的指针都进行追踪。
如下所示,假设F为一个局部变量指针,其引用了栈帧上的栈对象E→C→D→A, 因此说明栈对象E、C、D、A都是存活的,需要被扫描。 相反如果栈对象B没有被扫描,并且接下来在foo()函数中没有使用到B对象,那么B栈对象不会被扫描,从而解决了内存泄露问题。
参考:
https://zhuanlan.zhihu.com/p/321581866
https://cloud.tencent.com/developer/article/1583077
网友评论