常见内存逃逸情况
1、在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。
2、发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。
3、在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。
4、因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。
5、在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。
有大神总结了上述情况:多级间接赋值容易导致逃逸
这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go 语言中的引用类数据类型有 func
, interface
, slice
, map
, chan
, *Type(指针)
。
记住公式 Data.Field = Value
,如果 Data
, Field
都是引用类的数据类型,则会导致 Value
逃逸。这里的等号 =
不单单只赋值,也表示参数传递。
[]interface{}
:data[0] = 100
会导致100
逃逸map[string]interface{}
:data["key"] = "value"
会导致"value"
逃逸map[interface{}]interface{}
:data["key"] = "value"
会导致key
和value
都逃逸map[string][]string
:data["key"] = []string{"hello"}
会导致切片逃逸map[string]*int
: 赋值时*int
会 逃逸[]*int
:data[0] = &i
会使i
逃逸func(*int)
:data(&i)
会使i
逃逸func([]string)
:data([]{"hello"})
会使[]string{"hello"}
逃逸chan []string
:data <- []string{"hello"}
会使[]string{"hello"}
逃逸- 以此类推,不一一列举了
func test(i int) {}
func testEscape(i *int) {}
func main() {
i, j, m, n := 0, 0, 0, 0
t, te := test, testEscape // 函数变量
// 直接调用
test(m) // 不逃逸
testEscape(&n) // 不逃逸
// 间接调用
t(i) // 不逃逸
te(&j) // 逃逸
}
如何避免
1、go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口。
2、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。
3、预先设定好slice长度,避免频繁超出容量,重新分配。
使用内存逃逸分析工具检查代码
go build -gcflags '-m -l' main.go
/tmp/main.go:11:9: &i escapes to heap
/tmp/main.go:11:9: from ~r0 (return) at /tmp/main.go:11:2
/tmp/main.go:10:2: moved to heap: i
/tmp/main.go:16:18: heap() escapes to heap
/tmp/main.go:16:18: from ... argument (arg to ...) at /tmp/main.go:16:13
/tmp/main.go:16:18: from *(... argument) (indirection) at /tmp/main.go:16:13
/tmp/main.go:16:18: from ... argument (passed to call[argument content escapes]) at /tmp/main.go:16:13
/tmp/main.go:16:13: main ... argument does not escape
日志中的 &i escapes to heap
表示该变量数据逃逸到了堆上。