逃逸分析
逃逸分析(Escape Analysis)是指由编译器决定内存分配的位置,不需要程序员决定。
- 在函数中申请一个新的对象
- 如果分配在栈上,则函数执行结束后可自动将内存回收
- 如果分配在堆上,则函数执行结束后可交给GC(垃圾回收)处理
- 有了逃逸分析,返回函数局部变量变得可能
- 逃逸分析还跟闭包息息相关
1.逃逸策略
在函数中申请新对象时,编译器会根据该对象是否被函数外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中
- 如果函数外部存在引用,则必定放到堆中
- 注意点:对于仅在函数内部使用的变量,也有可能放入堆中,如内存过大超过栈的存储能力。
2. 逃逸场景
我们可以通过编译参数-gcflags=-m
来查看编译过程中的逃逸分析过程
例如go build -gcflags=-m
2.1 指针逃逸
go
type Student struct {
Name string
Age int
}
func StudentRegister(name string,age int) *Student{
s := new(Student) // 局部变量s逃逸到堆中
s.Name = name
s.Age = age
return s
}
func main(){
StudentRegister("Jim", 18)
}
函数StudentRegister()内部的s为局部变量,其值通过函数返回值返回,s本身为一个指针,其指向的内存地址不会是栈而是堆。
2.2 栈空间不足逃逸
go
func Slice(){
s := make([]int,1000,1000)
for index,_ := range s{
s[index] = index
}
}
func main(){
Slice()
}
Slice函数分配了一个长度为1000的切片,是否逃逸取决于栈的空间是否足够大。
当切片长度不断增加到10000时就会发生逃逸
- 实际上当栈的空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中
2.3 动态类型逃逸
很多函数的参数为interface类型,比如fmt.Println(a ...interface{}),编译期间很难确定其参数具体类型,也会产生逃逸。
go
func main(){
s := "Escape"
fmt.Println(s)
}
2.4 闭包引用对象逃逸
go
func Fibonacci() func() int {
a,b := 0,1
return func() int {
a, b = b,a+b
return a
}
}
func main(){
f := Fibonacci()
for i:=0;i<10;i++{
fmt.Printf("Fibonacci: %d\n",f())
}
}
该函数返回一个闭包,闭包使用了函数的局部变量a,b,使用时通过该函数获取闭包,然后每次执行闭包都会一次输出Fibonacci数列。
Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将两者放到堆上,以至于产生了逃逸。
3. 小结
- 栈上分配内存比在堆上分配内存有更高的效率
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析的目的是决定分配地址是栈还是堆
- 逃逸分析在编译阶段完成