一般来说,局部变量会在函数返回后被销毁,因此被返回的引用就成为了没有指针指向的引用,程序会进入未知状态 但这在go中是安全的,go语言会对每个局部变量进行逃逸分析,如果发生局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上,即使释放函数,其内容也不会受影响
概念: go在 编译时 进行逃逸分析,他会决定一个对象放在栈上还是堆上,不逃逸的放栈上,可能逃逸的放堆上
目的:尽可能将变量分配到栈上
方式: 编译器可以证明变量在函数返回后不再被引用,才会分配到栈上,否则分配到堆上
- 在栈上分配(静态内存分配),一般由系统进行申请和释放,eg.函数的入参、局部变量、返回值等,每个函数都会分配一个栈帧,在函数运行结束后进行销毁
- 在堆上分配(动态内存分配),在函数运行结束后仍然可以使用,如果要回收掉,需要进行GC,带来额外的性能开销
编程语言不断优化GC算法,主要目的是为了减少GC带来的额外性能开销,变量一旦逃逸会导致性能开销变大
逃逸机制:
编译器会根据变量是否被外部引用来决定是否逃逸
1.函数没有外部引用,优先放到栈中
2.函数外部存在引用,放在堆中
3.栈上放不下,必定放到堆中
逃逸分析就是由编译器决定哪些变量放在栈中,哪些放在堆中,通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析
(二)可能的场景:
通过go build -gcflags=-m main.go查看逃逸情况
1.指针逃逸
函数返回值为局部变量的指针,函数虽然退出了,但因为指针的存在,指向的内存不能随着函数结束而回收,因此只能分配在堆上
package main func escape1()* int{ var a int = 1 return &a } func main(){ escape1() }
2.栈空间不足
package main func escape2() { s := make([]int, 0, 10000) for index, _ := range s { s[index] = index } } func main() { escape2() }
3.变量大小不确定
package main func escape3() { number := 10 s := make([]int, number) for i := 0; i < len(s); i++ { s[i] = i } } func main() { escape3() }
编译期无法确定slice的长度,这种情况为了保证内存的安全,编译期也会触发逃逸,在堆上进行内存分配
直接s:=make([]int,10)不会发生逃逸
4.动态类型
动态类型就是编译期间不确定参数的类型、参数的长度也不确定的情况下就会发生逃逸
空接口interface{}可以表示任意的类型,如果函数参数为interface{},编译期间很难确定其参数的具体类型,也会发生逃逸
package main import "fmt" func escape4() { fmt.Println(1111) } func main() { escape4() }
5.闭包引用对象
闭包函数中局部变量i在后续函数是继续使用的,编译器将其分配到堆上
package main func escape5() func() int { var i int = 1 return func() int { i++ return i } } func main() { escape5() }
总结:
1.栈上分配内存比堆中分配内存效率更高
2.栈上分配的内存不需要GC处理,而堆需要
3.逃逸分析目的是决定内存分配地址是栈还是堆
4.逃逸分析在编译阶段完成
无论变量大小,只要是指针变量都会在堆上分配,所以对于小变量使用传值(而不是传指针)效率更高
参考:GOLANG-ROADMAP