逃逸分析 #
1.1. 什么是逃逸分析? #
逃逸分析(Escape analysis): 由语言决定变量分配到堆上还是栈上。在Java中,逃逸分析是在运行时发生;在Go语言中,逃逸分析在编译期间完成,编译器决定内存分配的位置,不需要程序员指定。
在函数中申请一个新的对象:
- 如果分配在栈中,则函数执行结束可自动将内存回收;
- 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;
逃逸分析针对指针和大对象:除大对象外,一个值引用变量如果没有被取址,那么它永远不可能逃逸。
1.2. 逃逸场景 #
指针逃逸、动态类型逃逸、闭包引用对象逃逸属于指针逃逸,都会发生指针的传递;栈空间不足逃逸属于大对象逃逸,不一定有值传递,这种场景是由于对象过大,无法在栈上分配导致。
-
指针逃逸: Go可以返回局部变量指针,示例代码如下:
package main type User struct { Name string } func main() { user := structFunc() user.Name = "2" } func structFunc() *User { // 局部变量user逃逸到堆 user := &User{ Name: "123", } user.Name = "234" return user }
user 本身为一指针,其值通过函数返回值返回,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例,如果返回值不是指针而是值,此时会发生值拷贝,不会出现逃逸分析。
-
动态类型逃逸(不确定长度大小):很多函数参数为interface类型,比如fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也能产生逃逸。
如下代码所示:
package main import "fmt" func main() { s := "Escape" fmt.Println(s) }
-
闭包引用对象逃逸:
package main import "fmt" 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()) } }
Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸。
-
栈空间不足逃逸(空间开辟过大):
package main func Slice() { s := make([]int, 10000, 10000) for index, _ := range s { s[index] = index } } func main() { Slice() }
当切片长度扩大到10000时就会逃逸。
实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。
1.3. 逃逸分析的作用 #
- 逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
- 逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配 ,而没有发生逃逸的则有编译器在栈上分配)。
- 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
1.4. 总结 #
- 栈上分配内存比在堆中分配内存有更高的效率
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析目的是决定内分配地址是栈还是堆
- 逃逸分析在编译阶段完成