堆/栈
- 堆(Heap):一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多。
- 栈(Stack):由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上。
申请到栈内存好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。
申请到堆内存会引起垃圾回收,如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。
逃逸分析
在编译阶段确立逃逸,注意并不是在运行时。
在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:
- 是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上。
- 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上。
- 动态分配不定空间,编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。
所以逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。
在函数中申请一个新的对象:
- 如果分配在栈中,则函数执行结束可自动将内存回收;
- 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;
为什么需要逃逸
如果变量都分配到堆上了会出现什么事情?例如:
- 垃圾回收(GC)的压力不断增大。
- 申请、分配、回收内存的系统开销增大(相对于栈)。
- 动态分配产生一定量的内存碎片。
怎么查看逃逸
通过编译器命令,就可以看到详细的逃逸分析过程
go build -gcflags '-m -l' main.go
- -m 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了。
- -l 会禁用函数内联,在这里禁用掉 inline 能更好的观察逃逸情况,减少干扰。
逃逸场景(什么情况才分配到堆中)
1. 返回局部变量地址(指针)
package main type User struct { Name string Id int } func Test() *User{ a:=&User{} return a } func main() { Test() }
$ go build -gcflags '-m -l' main.go # command-line-arguments .\main.go:9:5: &User{} escapes to heap
通过查看分析结果,可得知 &User 逃到了堆里,也就是分配到堆上了。
编译器认为函数外会用到这个局部变量,所以分配到堆上,如果分配到栈上,函数结束后,函数外引用的就是一个非法的地方了。
2. 编译阶段不能确定接口的动态类型(interface{})
package main type Inter1 interface { A() } type Inter2 interface { B() } type User struct { Name string Id int } func (u *User) A() { } func (u User) B() { } func Test1() interface{} { var a interface{} a = &User{} return a } func Test2() Inter1 { var b Inter1 b = &User{} return b } func Test3() Inter2 { var b Inter2 b = User{} return b } func main() { Test1() Test2() Test3() }
$ go build -gcflags '-m -l' main.go # command-line-arguments .\main.go:16:7: u does not escape .\main.go:19:7: u does not escape .\main.go:24:6: &User{} escapes to heap .\main.go:30:5: &User{} escapes to heap .\main.go:36:3: User{} escapes to heap <autogenerated>:1: leaking param: .this <autogenerated>:1: .this does not escape <autogenerated>:1: leaking param: .this
编译阶段不能确定接口的动态类型,所以要分配在堆上
3. 栈空间不足导致逃逸(空间开辟过大)
package main func Slice() { s := make([]int, 1000, 1000) for index, _ := range s { s[index] = index } s1 := make([]int, 1000, 10000) for index, _ := range s { s1[index] = index } } func main() { Slice() }
$ go build -gcflags '-m -l' main.go # command-line-arguments .\main.go:4:11: make([]int, 1000, 1000) does not escape .\main.go:9:12: make([]int, 1000, 10000) escapes to heap
是否逃逸取决于栈空间是否足够大
4. 无法判断当前切片长度时
package main func Slice() { len := 1 s := make([]int, len, len) for index, _ := range s { s[index] = index } } func main() { Slice() }
$ go build -gcflags '-m -l' main.go # command-line-arguments .\main.go:5:11: make([]int, len, len) escapes to heap
实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。
func Test() { a := make([]int, 0, 20) // 栈 空间小 不逃逸 b := make([]int, 0, 20000) // 堆 空间过大 逃逸 l := 20 c := make([]int, 0, l) // 堆 动态分配不定空间 逃逸 }
5. 闭包捕获变量并修改导致变量逃逸
直接看专栏这篇博文Golang底层原理剖析之闭包
逃逸总结
- 静态分配到栈上,性能一定比动态分配到堆上好。
- 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心。
- 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)。
- 直接通过 go build -gcflags ‘-m -l’ 就可以看到逃逸分析的过程和结果。
- 到处都用指针传递并不一定是最好的,要用对。
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析目的是决定内分配地址是栈还是堆
- 逃逸分析在编译阶段完成