go 混合写屏障

2023-01-27  本文已影响0人  overflow_e4e4

go 混合写屏障:

  1. GC开始时,STW,对栈进行扫描,将所有可达对象置黑
  2. GC期间,任何栈上新创建的对象,皆置黑【1和2配合,可避免插入屏障对栈区的二次三色标记】
  3. 被删除的对象置灰
  4. 新挂载的对象置灰

疑问:栈上的对象如果是随着栈回收而被回收的话,为啥混合写屏障还会有对栈上内存的规则?
答案:(疑似)
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

上一篇 下一篇

猜你喜欢

热点阅读