Golang内存管理—垃圾收集器:栈对象处理
0. 简介
上一篇博客,我们基本介绍了Golang
的GC
原理,这里我们触及一下GC
过程中栈对象的处理。其实问题的起因在于我最开始对栈对象在GC
时的生存周期的误解,最初我认为:栈对象会在栈返回后才会自动消失!但是在Golang基础数据结构—字符串的unsafeStringToBytes
测试时,我通过-gcflags='-m'
发现字符串s
并没有逃逸到堆上,建立在我一上认知,我觉得它不会被GC呀,但是测试结果打了我的脸。所以后续我就对Golang
的GC
时对栈对象的处理进行了一番探究。
1. 栈对象跟踪
栈对象是在栈上能够被寻址的对象,其一定在栈上有地址。所以就被叫做栈对象
。因为并不是所有的变量都会存储在栈上,例如存储在寄存器中的变量就是不能被寻址的。
具体说明可以参考Golang
源码:src/runtime/mgcstack.go。我这里就不完全翻译其说明了,只说一下我理解的栈跟踪步骤,如果有问题,还麻烦大家指出。
1.1 栈对象活性分析
在Go
的编译器编译代码的最后一步——生产机器码时,会完成其重要的工作——栈布局(stack frame layout)
,其将栈便宜位置分配给局部变量,以及指针活性分析(pointer liveness analysis)
,后者计算每个垃圾回收安全点上的那些栈指针仍然是活动的。具体参考Go 编译器介绍。
也就是前面说过的unsafeStringToBytes
函数中s
在GC
点已经不存活了。
// 不安全的转换方法
func unsafeStringToBytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
sliceHeader := &reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
runtime.GC()
time.Sleep(1 * time.Nanosecond)
b := *(*[]byte)(unsafe.Pointer(sliceHeader))
return b
}
而如果我们在return b
之前加上runtime.KeepAlive(s)
后,就可以避免s
被GC
掉,其实所谓runtime.KeepAlive
所做的事情就是使一个变量”存活“到其所在的位置被执行,具体做法是Go
编译器会设置一个名为 OpKeepAlive
的静态单赋值(SSA),然后剩余的编译就会知道将这个变量的存活期保证到使用了 runtime.KeepAlive()
的时刻。
1.2 栈跟踪
单纯的栈对象活性分析并不能满足所有的需求,比如该栈对象的指针指向其他非存活栈对象,那么很可能会漏掉,所以就需要栈跟踪来保证找到剩下的需要存活的对象了。
比如以下的栈:
+----------+
| foo() |
| +------+ |
| | A | | <---\
| +------+ | |
| | |
| +------+ | |
| | B | | |
| +------+ | |
| | |
+----------+ |
| bar() | |
| +------+ | |
| | C | | <-\ |
| +----|-+ | | |
| | | | |
| +----v-+ | | |
| | D ---------/
| +------+ | |
| | |
+----------+ |
| baz() | |
| +------+ | |
| | E -------/
| +------+ |
| ^ |
| F: --/ |
| |
+----------+
foo()
调用bar()
,bar()
调用baz()
,每个方法再在栈上都体现出一段帧;foo()
中有栈对象A
和B
;bar()
中有栈对象C
和D
,C
指向D
,D
和A
;baz()
中有栈对象E
,指向C
,还有个局部变量F
指向E
;
从存活的局部变量F
开始,经过扫描,最终会将E
、C
、D
、A
扫描到,但是B
不是被扫描到,因为没有指向其的存活指针,同理,B
指向堆的指针(如果有的话)也就不被认为是存活的。
转载自:https://juejin.cn/post/7239542388603600951