概念内存逃逸:栈上的内存逃逸到了堆上的现象就称为内存逃逸
程序在编译阶段根据代码来确认哪些变量分配在栈区,哪些变量分配在堆区。这样可以防止过多内存在堆上分配,减轻GC压力以及程序STW的时间
原理指向栈对象的指针不能存储在堆中
指向栈对象的指针不能超过该对象的存活期,也就是指针不能再栈对象被销毁后依然存活
如何查看通过如下方式编译代码可以看到逃逸分析
go build -gcflags='-m -m -l'
-m -m 能看到所有编译器优化-l 禁用掉内联优化
分析例子
函数返回局部变量指针
package main
type st struct {
a int
b int
}
func Add(x, y int) *st {
r := st{a: x, b: y}
return &r
}
func main() {
res := Add(3, 5)
res.a = 10
}
查看逃逸分析结果
$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:9:2: r escapes to heap:
./test.go:9:2: flow: ~r2 = &r:
./test.go:9:2: from &r (address-of) at ./test.go:10:9
./test.go:9:2: from return &r (return) at ./test.go:10:2
./test.go:9:2: moved to heap: r
note: module requires Go 1.17
可以看到局部变量指针r发生了内存逃逸
函数返回局部变量package main
type st struct {
a int
b int
}
func Add(x, y int) st {
r := st{a: x, b: y}
return r
}
func main() {
res := Add(3, 5)
res.a = 10
}
逃逸分析
$ go build -gcflags="-m -m -l" test.go
$
没有任何信息,说明没有发生逃逸
interface类型逃逸
package main
import "fmt"
func main() {
res := 10
fmt.Print(res)
}
逃逸分析
$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:7:11: res escapes to heap:
./test.go:7:11: flow: {storage for ... argument} = &{storage for res}:
./test.go:7:11: from res (spill) at ./test.go:7:11
./test.go:7:11: from ... argument (slice-literal-element) at ./test.go:7:11
./test.go:7:11: flow: {heap} = {storage for ... argument}:
./test.go:7:11: from ... argument (spill) at ./test.go:7:11
./test.go:7:11: from fmt.Print(... argument...) (call parameter) at ./test.go:7:11
./test.go:7:11: ... argument does not escape
./test.go:7:11: res escapes to heap
note: module requires Go 1.17
可以看到res发生了逃逸,这是因为:
编译器很难确定interface{}的具体类型,所以会发生逃逸但是我们可以看到res本身没有move to heap
,这是因为编译器只是把res存储的值存储到了堆区,res本身依然还是在栈区,但是我们可以做以下修改:
package main
import "fmt"
func main() {
res := 10
fmt.Print(&res)
}
逃逸分析
$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:6:2: res escapes to heap:
./test.go:6:2: flow: {storage for ... argument} = &res:
./test.go:6:2: from &res (address-of) at ./test.go:7:12
./test.go:6:2: from &res (interface-converted) at ./test.go:7:12
./test.go:6:2: from ... argument (slice-literal-element) at ./test.go:7:11
./test.go:6:2: flow: {heap} = {storage for ... argument}:
./test.go:6:2: from ... argument (spill) at ./test.go:7:11
./test.go:6:2: from fmt.Print(... argument...) (call parameter) at ./test.go:7:11
./test.go:6:2: moved to heap: res
./test.go:7:11: ... argument does not escape
note: module requires Go 1.17
会发现,res本身也逃逸到了堆区,这是因为fmt.Print
输出的是res的地址,因此编译器会将地址的值存储到了堆区,但是堆上的对象不能存储一个栈上的地址,因为栈变量销毁后,其地址就无效了,那堆区中存储的地址也会无效,因此需要将res也逃逸到堆区。
package main
func add() func() int {
num := 0
f := func() int {
num++
return num
}
return f
}
func main() {
add()
}
逃逸分析:
$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:6:3: add.func1 capturing by ref: num (addr=true assign=true width=8)
./test.go:5:7: func literal escapes to heap:
./test.go:5:7: flow: f = &{storage for func literal}:
./test.go:5:7: from func literal (spill) at ./test.go:5:7
./test.go:5:7: from f := func literal (assign) at ./test.go:5:4
./test.go:5:7: flow: ~r0 = f:
./test.go:5:7: from return f (return) at ./test.go:9:2
./test.go:4:2: num escapes to heap:
./test.go:4:2: flow: {storage for func literal} = &num:
./test.go:4:2: from func literal (captured by a closure) at ./test.go:5:7
./test.go:4:2: from num (reference) at ./test.go:6:3
./test.go:4:2: moved to heap: num
./test.go:5:7: func literal escapes to heap
note: module requires Go 1.17
可以看到f和闭包中的num都发生了内存逃逸。因为函数也是一个指针类型,因此当做返回值时也发生了逃逸。而只要调用了f,就会调用num,因此num也需要逃逸到堆区
变量大小不确定或者栈空间不足 当栈空间足够时,不会发生逃逸,但是当变量过大时,已经完全超过栈空间的大小时,将会发生逃逸到堆上分配内存初始化切片时,没有直接指定大小,而是填入的变量,这种情况为了保证内存的安全,编译器也会触发逃逸,在堆上进行分配内存 向channel发送指针数据package main
func main() {
ch1 := make(chan *int, 1)
y := 5
py := &y
ch1 <- py
}
逃逸分析
$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:5:2: y escapes to heap:
./test.go:5:2: flow: py = &y:
./test.go:5:2: from &y (address-of) at ./test.go:6:8
./test.go:5:2: from py := &y (assign) at ./test.go:6:5
./test.go:5:2: flow: {heap} = py:
./test.go:5:2: from ch1 <- py (send) at ./test.go:7:6
./test.go:5:2: moved to heap: y
note: module requires Go 1.17
编译器无法知道channel 中的数据会被哪个 goroutine 接收,无法知道什么时候释放
总结 逃逸分析在编译阶段确定哪些变量可以分配在栈上,哪些变量分配在堆上减轻了GC压力,提供程序的运行速度栈上内存使用完毕不需要GC处理,堆上内存使用完毕会交给GC处理尽量减少逃逸代码,减轻GC压力欢迎分享,转载请注明来源:内存溢出
评论列表(0条)