常见内存逃逸情况
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 表示该变量数据逃逸到了堆上。
3096

被折叠的 条评论
为什么被折叠?



