Golang学习之内存逃逸分析
某不知名Gopher 人气:0在开始剖析Go逃逸分析前,我们要先清楚什么是堆栈。数据结构中有堆栈,内存分配中也有堆栈,两者在定义和用途上虽不同,但也有些许关联,内存分配中栈的压栈和出栈操作,类似于数据结构中的栈的操作方式
内存分配中的堆栈
程序在运行过程中,必不可少的会使用变量、函数和数据,变量和数据在内存中存储的位置可以分为:堆区(Heap)和栈区(Stack),一般由C或C++编译的程序占用内存为:
- 栈区
- 堆区
- 全局区
- 常量区
- 程序代码区
软件程序中的数据和变量都会被分配到程序所在的虚拟内存空间中
栈
每个函数都有自己独立的栈空间,函数的调用参数、返回值以及局部变量大都被分配到该函数的栈空间中, 这部分内存由编译器进行管理,编译时确定分配内存的大小。栈空间有特定的结构和寻址方式,所以寻址十分迅速、开销小,只需要2条 CPU 指令,即压栈出栈 PUSH
和 RELEASE
,由于函数栈内存的大小在编译时确定, 所以当局部变量数据太大就会发生栈溢出(Stack Overflow)。当函数执行完毕后, 函数的栈空间被回收, 无需手动去释放。
区别于堆空间,通过 malloc
出来的内存,函数执行完毕后需要“手动”释放,“手动”释放在有垃圾回收的语言中,表现为垃圾回收系统,比如 Golang 语言的 GC 系统,GC 系统通过标记等手段,识别出需要回收的空间。
堆
堆空间没有特定的结构,也没有固定的大小,可以动态进行分配和调整,所以内存占用较大的局部变量会放在堆空间上,在编译时不知道该分配多少大小的变量,在运行时也会分配到堆上,在堆上分配内存开销比在栈上大,而且堆上分配的内存需要手动释放,对于 Golang 这种有 GC 机制的语言, 也会增加 GC 压力, 也容易造成内存碎片。
注:栈是线程级的,堆是进程级的
内存逃逸
所谓内存逃逸,就是本该分配于栈空间的变量,被分配到了堆空间,过多的内存逃逸会导致GC压力变大,堆空间碎片化。
Go语言中,变量不能显示的指定分配在栈空间还是堆空间,但是官方回复中大致表示了一个原则:如果局部变量被其他函数捕获,那么就分配在堆上。
逃逸分析
在编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析,通俗来说,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。逃逸分析有两个基本的不变性:
- 指向栈对象的指针不能存储在堆中
- 指向栈对象的指针不能超过该栈对象的存活期(即指针不能在栈对象被销毁后依旧存活)
分析工具
通过编译工具查看详细的逃逸分析过程 go build -gcflags '-m -l' xxx.go
编译参数(-gcflags):
- -N:禁止编译优化
- -l:禁止内联
- -m:逃逸分析
- -benchmem:压测时打印内存分配统计
通过逃逸分析判断一个变量到底是分配在堆上还是栈上
逃逸场景
指针逃逸
指针逃逸应该是最容易理解的一种情况了,即在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。
// main.go package main import "fmt" type Demo struct { name string } func createDemo(name string) *Demo { d := new(Demo) // 局部变量 d 逃逸到堆 d.name = name return d } func main() { demo := createDemo("demo") fmt.Println(demo) }
在这个例子中,函数createDemo
的局部变量d发生了逃逸,d作为返回值在main函数中继续使用,因此d指向的内存不能分配在栈上,只能分配在堆上,借助分析工具查看逃逸情况
$ go build -gcflags=-m main.go ./main.go:10:6: can inline createDemo ./main.go:17:20: inlining call to createDemo ./main.go:18:13: inlining call to fmt.Println ./main.go:10:17: leaking param: name ./main.go:11:10: new(Demo) escapes to heap ./main.go:17:20: new(Demo) escapes to heap //指针逃逸 ./main.go:18:13: demo escapes to heap //interface{}动态类型逃逸 ./main.go:18:13: main []interface {} literal does not escape ./main.go:18:13: io.Writer(os.Stdout) escapes to heap <autogenerated>:1: (*File).close .this does not escape
escapes to heap
表示逃逸到堆上了
动态反射interface{}变量
在 Go 语言中,接口即 interface{}
可以表示任意的类型,如果函数参数为 interface{}
,编译期间很难确定其参数的具体类型,也会发生逃逸。仍以上面的例子
func main() { demo := createDemo("demo") fmt.Println(demo) } ./main.go:18:13: demo escapes to heap
demo
是main函数的一个局部变量,该变量作为实参传递给fmt.Println()
,但是因为fmt.Println()
的参数类型是interface{}
,因此也发生了逃逸
解释:fmt.Println
之类的底层系统函数,实现逻辑会基于interface{}
做反射,通过 reflect.TypeOf(arg).Kind()
获取接口对象的底层数据类型,创建具体类型对象时,会发生内存逃逸。由于 interface{}
的变量,编译时无法确定变量类型以及申请空间大小,所以不能在栈空间上申请内存,需要在 runtime
时动态申请,理所应当地发生内存逃逸。
申请栈空间过大
栈空间大小是有限的,如果编译时发现局部变量申请的空间过大,则会发生内存逃逸,在堆空间上给大变量分配内存
func main() { num := make([]int, 0, 10000) _ = num } .\main.go:404:13: make([]int, 0, 10000) escapes to heap //发生逃逸
经过测试,num := make([]int, 0, 8193)
时刚好发生内存逃逸。在 64 位机上 int
类型为 8B,即 8192 * 8B = 64KB
func main() { num1 := make([]int, 0, 8192) _ = num1 num2 := make([]int, 0, 8193) _ = num2 } .\main.go:404:14: make([]int, 0, 8192) does not escape .\main.go:407:14: make([]int, 0, 8193) escapes to heap
切片变量自身和元素的逃逸
1.未指定slice的len
和cap
时,slice自身未发生逃逸,slice的元素发生逃逸。因此slice会动态扩容,编译器不知道容量大小,无法提前在栈空间分配内存,扩容后slice的元素可能会被分配到堆空间,所以slice容器自身也不能被分配到栈空间
type person struct { Name string } func main() { var num []*person p1 := &person{ Name: "ss", } num = append(num, p1) } .\main.go:409:8: &person{...} escapes to heap
2.只指定slice的长度即array,数组本身和元素均在栈上分配,均未发生逃逸
闭包
所谓闭包,就是函数与其所处环境捆绑的组合,也就是说,闭包可以让你在一个内部函数中访问到其外部函数的作用域
func Increase() func() int { n := 0 return func() int { n++ return n } } func main() { in := Increase() fmt.Println(in()) // 1 fmt.Println(in()) // 2 }
Increase()
返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in
被销毁。很显然,变量 n 占用的内存不能随着函数 Increase()
的退出而回收,因此将会逃逸到堆上。
.\main.go:408:2: moved to heap: n .\main.go:409:9: func literal escapes to heap .\main.go:417:13: ... argument does not escape .\main.go:417:16: in() escapes to heap .\main.go:418:13: ... argument does not escape .\main.go:418:16: in() escapes to heap
逃逸分析的作用
- 通过逃逸分析能确定哪些变量分配到栈空间,哪些分配到堆空间,对空间需要 GC 系统回收资源,GC 系统会有微秒级的 STW,降低 GC 的压力能提高系统的运行效率。
- 栈空间的分配比堆空间更快性能更好,对于热点数据分配到栈上能提高接口的响应。
- 栈空间分配的内存,在函数执行完毕后由系统回收资源,不需要 GC 系统参与,也不需要 GC 标记清除,可降低内存的占用
加载全部内容