概述
编译器通过逃逸分析技术去选择堆或者栈,逃逸分析的基本思想如下:检查变量的生命周期是否是完全可知的,如果通过检查,则可以在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配
逃逸分析原则
如果变量在函数外部没有引用,则优先放到栈中
如果变量在函数外部存在引用,则必定放在堆中
场景分析
变量类型不确定
package main
import "fmt"
func main() {
a := 666
fmt.Println(a)
}
逃逸分析结果
[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_2.go
# command-line-arguments
./demo_escapes_2.go:7:14: a escapes to heap:
./demo_escapes_2.go:7:14: flow: {storage for ... argument} = &{storage for a}:
./demo_escapes_2.go:7:14: from a (spill) at ./demo_escapes_2.go:7:14
./demo_escapes_2.go:7:14: from ... argument (slice-literal-element) at ./demo_escapes_2.go:7:13
./demo_escapes_2.go:7:14: flow: {heap} = {storage for ... argument}:
./demo_escapes_2.go:7:14: from ... argument (spill) at ./demo_escapes_2.go:7:13
./demo_escapes_2.go:7:14: from fmt.Println(... argument...) (call parameter) at ./demo_escapes_2.go:7:13
./demo_escapes_2.go:7:13: ... argument does not escape
./demo_escapes_2.go:7:14: a escapes to heap
分析结果告诉我们变量a逃逸到了堆上。但是,我们并没有外部引用啊,为啥也会有逃逸呢?
a逃逸是因为它被传入了fmt.Println的参数中,这个方法参数自己发生了逃逸。
func Println(a ...interface{}) (n int, err error)
因为fmt.Println的函数参数为interface类型,编译期不能确定其参数的具体类型,所以将其分配于堆上
对于上述的情况,更改代码。就不会出现逃逸
package main
func main() {
a := 666
println(a)
// fmt.Println(a)
}
逃逸分析
[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_2.go
[root@master 20230915]#
暴露给外部指针
package main
func foo() *int {
a := 666
return &a
}
func main() {
_ = foo()
}
逃逸分析
[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_3.go
# command-line-arguments
./demo_escapes_3.go:4:2: a escapes to heap:
./demo_escapes_3.go:4:2: flow: ~r0 = &a:
./demo_escapes_3.go:4:2: from &a (address-of) at ./demo_escapes_3.go:5:9
./demo_escapes_3.go:4:2: from return &a (return) at ./demo_escapes_3.go:5:2
./demo_escapes_3.go:4:2: moved to heap: a
这种情况直接满足我们上述中的原则:变量在函数外部存在引用
因为当函数执行完毕,对应的栈帧就被销毁,但是引用已经被返回到函数之外
如果这时外部从引用地址取值,虽然地址还在,但是这块内存已经被释放回收了,这就是非法内存,问题可就大了
变量所占内存较大
package main
func foo() {
s := make([]int, 10000, 10000)
for i := 0; i < len(s); i++ {
s[i] = i
}
}
func main() {
foo()
}
逃逸分析
# command-line-arguments
./demo_escapes_4.go:4:11: make([]int, 10000, 10000) escapes to heap:
./demo_escapes_4.go:4:11: flow: {heap} = &{storage for make([]int, 10000, 10000)}:
./demo_escapes_4.go:4:11: from make([]int, 10000, 10000) (too large for stack) at ./demo_escapes_4.go:4:11
./demo_escapes_4.go:4:11: make([]int, 10000, 10000) escapes to heap
当我们创建了一个容量为10000的int类型的底层数组对象时,由于对象过大,它也会被分配到堆上
变量大小不确定
package main
func foo() {
n := 1
s := make([]int, n)
for i := 0; i < len(s); i++ {
s[i] = i
}
}
func main() {
foo()
}
逃逸分析结果
[root@master 20230915]# go build -gcflags '-m -m -l' demo_escapes_5.go
# command-line-arguments
./demo_escapes_5.go:5:14: make([]int, n) escapes to heap:
./demo_escapes_5.go:5:14: flow: {heap} = &{storage for make([]int, n)}:
./demo_escapes_5.go:5:14: from make([]int, n) (non-constant size) at ./demo_escapes_5.go:5:14
./demo_escapes_5.go:5:14: make([]int, n) escapes to heap
在make方法中,没有直接指定大小,而是填入了变量n,这时Go逃逸分析也会将其分配到堆区去
可见,为了保证内存的绝对安全,Go的编译器可能会将一些变量不合时宜地分配到堆上,但是因为这些对象最终也会被垃圾收集器处理