type
status
date
slug
summary
tags
category
icon
password
什么是内存逃逸
在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在
堆上分配
,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸
。典型场景
- 函数返回局部指针变量。 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
- 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
- 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
- interface动态类型逃逸。
interface{}
可以表示任意的类型,如果函数参数为interface{}
,编译期间很难确定其参数的具体类型,也会发生逃逸。
- 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
- 变量大小不确定及栈空间不足引发逃逸。我们可以看到,当栈空间足够时,不会发生逃逸,但是当变量过大时,已经完全超过栈空间的大小时,将会发生逃逸到堆上分配内存。同样当我们初始化切片时,没有直接指定大小,而是填入的变量,这种情况为了保证内存的安全,编译器也会触发逃逸,在堆上进行分配内存。
举例
通过
go build -gcflags=-m
可以查看逃逸的情况,使用 go build -gcflags="-m -m -l"
可以查看到更详细的逃逸分析的结果。-m -m
查看编译器的所有优化,-l
禁用掉内联优化。函数返回局部指针变量
package main import "fmt" type A struct { s string } func foo(s string) *A { a := new(A) a.s = s return a //返回局部变量a,在C语言中妥妥野指针,但在go则ok,但a会逃逸到堆 } func main() { a := foo("hello") b := a.s + " world" c := b + "!" fmt.Println(c) }
执行
go build -gcflags=-m main.go
# go build -gcflags=-m main.go # command-line-arguments .\main.go:11:6: can inline foo .\main.go:18:10: inlining call to foo .\main.go:21:13: inlining call to fmt.Println .\main.go:11:10: leaking param: s .\main.go:12:10: new(A) escapes to heap .\main.go:18:10: new(A) does not escape .\main.go:19:11: a.s + " world" does not escape .\main.go:20:9: b + "!" escapes to heap .\main.go:21:13: c escapes to heap .\main.go:21:13: []interface {}{...} does not escape <autogenerated>:1: leaking param content: .this <autogenerated>:1: .this does not escape
.\main.go:12:10: new(A) escapes to heap
说明new(A)
逃逸了,符合上述提到的常见情况中的第一种。
.\main.go:19:11: a.s + " world" does not escape
说明b
变量没有逃逸,因为它只在方法内存在,会在方法结束时被回收。
.\main.go:20:9: b + "!" escapes to heap
说明c
变量逃逸,通过fmt.Println(a ...interface{})
打印的变量,都会发生逃逸。
interface类型逃逸
func main() { str := "This is str" fmt.Printf("%v",str) }
逃逸分析结果:
go build -gcflags="-m -m -l" ./main.go # command-line-arguments .\main.go:7:13: str escapes to heap: .\main.go:7:13: flow: {storage for ... argument} = &{storage for str}: .\main.go:7:13: from str (spill) at .\main.go:7:13 .\main.go:7:13: from ... argument (slice-literal-element) at .\main.go:7:12 .\main.go:7:13: flow: {heap} = {storage for ... argument}: .\main.go:7:13: from ... argument (spill) at .\main.go:7:12 .\main.go:7:13: from fmt.Printf("%v", ... argument...) (call parameter) at .\main.go:7:12 .\main.go:7:12: ... argument does not escape .\main.go:7:13: str escapes to heap
str
是main
函数中的一个局部变量,传递给fmt.Println()
函数后发生了逃逸,这是因为fmt.Println()
函数的入参是一个interface{}
类型,如果函数参数为interface{}
,那么在编译期间就很难确定其参数的具体类型,也会发送逃逸。观察这个分析结果,我们可以看到没有
moved to heap: str
,这也就是说明str
变量并没有在堆上进行分配,只是它存储的值逃逸到堆上了,也就说任何被str
引用的对象必须分配在堆上。如果我们把代码改成这样:func main() { str := "This is str" fmt.Printf("%v", &str) }
逃逸分析结果:
go build -gcflags="-m -m -l" ./main.go # command-line-arguments .\main.go:6:2: str escapes to heap: .\main.go:6:2: flow: {storage for ... argument} = &str: .\main.go:6:2: from &str (address-of) at .\main.go:7:19 .\main.go:6:2: from &str (interface-converted) at .\main.go:7:19 .\main.go:6:2: from ... argument (slice-literal-element) at .\main.go:7:12 .\main.go:6:2: flow: {heap} = {storage for ... argument}: .\main.go:6:2: from ... argument (spill) at .\main.go:7:12 .\main.go:6:2: from fmt.Printf("%v", ... argument...) (call parameter) at .\main.go:7:12 .\main.go:6:2: moved to heap: str .\main.go:7:12: ... argument does not escape
这回
str
也逃逸到了堆上,在堆上进行内存分配,这是因为我们访问str
的地址,因为入参是interface
类型,所以变量str
的地址以实参的形式传入fmt.Printf
后被装箱到一个interface{}
形参变量中,装箱的形参变量的值要在堆上分配,但是还要存储一个栈上的地址,也就是str
的地址,堆上的对象不能存储一个栈上的地址,所以str
也逃逸到堆上,在堆上分配内存。(这里注意一个知识点:Go语言的参数传递只有值传递)闭包产生的逃逸
func Increase() func() int { n := 0 return func() int { n++ return n } } func main() { in := Increase() fmt.Println(in()) // 1 }
查看逃逸分析结果:
go build -gcflags="-m -m -l" .\main.go # command-line-arguments .\main.go:6:2: Increase capturing by ref: n (addr=false assign=true width=8) .\main.go:7:9: func literal escapes to heap: .\main.go:7:9: flow: ~r0 = &{storage for func literal}: .\main.go:7:9: from func literal (spill) at .\main.go:7:9 .\main.go:7:9: from return func literal (return) at .\main.go:7:2 .\main.go:6:2: n escapes to heap: .\main.go:6:2: flow: {storage for func literal} = &n: .\main.go:6:2: from n (captured by a closure) at .\main.go:8:3 .\main.go:6:2: from n (reference) at .\main.go:8:3 .\main.go:6:2: moved to heap: n .\main.go:7:9: func literal escapes to heap .\main.go:15:16: in() escapes to heap: .\main.go:15:16: flow: {storage for ... argument} = &{storage for in()}: .\main.go:15:16: from in() (spill) at .\main.go:15:16 .\main.go:15:16: from ... argument (slice-literal-element) at .\main.go:15:13 .\main.go:15:16: flow: {heap} = {storage for ... argument}: .\main.go:15:16: from ... argument (spill) at .\main.go:15:13 .\main.go:15:16: from fmt.Println(... argument...) (call parameter) at .\main.go:15:13.\main.go:15:13: ... argument does not escape .\main.go:15:16: in() escapes to heap
因为函数也是一个指针类型,所以匿名函数当作返回值时也发生了逃逸,在匿名函数中使用外部变量
n
,这个变量n
会一直存在直到in
被销毁,所以n
变量逃逸到了堆上。