基础
内存管理
逃逸分析
编译器会根据变量是否被外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中;
- 如果函数外部存在引用,则必定放到堆中;
- 如果栈上放不下,则必定放到堆上;
通过go build -gcflags=-m main.go查看逃逸情况:
逃逸分析也就是由编译器决定哪些变量放在栈,哪些放在堆中,通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析,发生逃逸的几种场景如下:
指针逃逸
函数返回值为局部变量的指针,函数虽然退出了,但是因为指针的存在,指向的内存不能随着函数结束而回收,因此只能分配在堆上。
package main
func escape1() *int {
var a int = 1
return &a
}
func main() {
escape1()
}
栈空间不足
当栈空间足够时,不会发生逃逸,但是当变量过大时,已经完全超过栈空间的大小时,将会发生逃逸到堆上分配内存。局部变量s占用内存过大,编译器会将其分配到堆上
package main
func escape2() {
s := make([]int, 0, 10000)
for index, _ := range s {
s[index] = index
}
}
func main() {
escape2()
}
变量大小不确定
编译期间无法确定slice的长度,这种情况为了保证内存的安全,编译器也会触发逃逸,在堆上进行分配内存。直接s := make([]int, 10)不会发生逃逸
package main
func escape3() {
number := 10
s := make([]int, number) // 编译期间无法确定slice的长度
for i := 0; i < len(s); i++ {
s[i] = i
}
}
func main() {
escape3()
}
动态类型
动态类型就是编译期间不确定参数的类型、参数的长度也不确定的情况下就会发生逃逸
空接口 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。
package main
import "fmt"
func escape4() {
fmt.Println(1111)
}
func main() {
escape4()
}
闭包引用对象
package main
func escape5() func() int {
var i int = 1
return func() int {
i++
return i
}
}
func main() {
escape5()
}
// 闭包函数中局部变量i在后续函数是继续使用的,编译器将其分配到堆上
总结
-
栈上分配内存比在堆中分配内存效率更高
-
栈上分配的内存不需要 GC 处理,而堆需要
-
逃逸分析目的是决定内分配地址是栈还是堆
-
逃逸分析在编译阶段完成
因为无论变量的大小,只要是指针变量都会在堆上分配,所以对于小变量我们还是使用传值效率(而不是传指针)更高一点。