逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。 当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或者去调用子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可能逃逸到被调用的子程序中。 如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中的任何一个地方被访问到——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,它们也可能发生逃逸,这种情况是当前程序中的指针逃逸。 逃逸分析需要确定指针所有可以存储的地方,保证指针的生命周期只在当前进程或线程中。

    逃逸分析的用处

    • 最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
    • 因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好
    • 同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

    go消除了堆和栈的区别
    go在一定程度消除了堆和栈的区别,因为go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。

    开启编译时的逃逸分析日志
    go run -gcflags=”-m -l” 1.go

    1. package main
    2. import (
    3. "fmt"
    4. )
    5. func main() {
    6. s := "hello"
    7. fmt.Println(s)
    8. }

    输出

    1. # command-line-arguments
    2. .\1.go:11:13: main ... argument does not escape
    3. .\1.go:11:13: s escapes to heap
    4. hello

    例子1

    1. package main
    2. type S struct{
    3. S1 int64
    4. }
    5. func identity() (*S,*S,S,int64,*int64) {
    6. var s1 S
    7. var s2 *S
    8. var s3 S
    9. var s4 *S
    10. s4.S1 = 1
    11. var a int64
    12. var b int64
    13. return &s1,s2,s3,a,&b
    14. }
    15. func main(){}
    16. 输出:
    17. # command-line-arguments
    18. .\1.go:8:6: moved to heap: s1
    19. .\1.go:14:6: moved to heap: b
    20. 结果解释:
    21. s1 为值类型,分配在栈上,但return时,返回的是s1的指针(引用类型),因此发生逃逸
    22. s2 为引用类型,且被return,直接分配在堆上
    23. s3 为值类型,分配在栈上,return时直接返回值(值类型),因此不会产生逃逸
    24. s4 为引用类型,但其生命周期只在其定义它的函数内,因此直接分配在栈上
    25. a 为值类型,分配在栈上,return时直接返回值(值类型),因此不会产生逃逸
    26. b 为值类型,分配在栈上,返回的是b的指针(引用类型),因此发生逃逸

    例子2

    1. package main
    2. type S struct{
    3. S1 int64
    4. }
    5. func identity(b *int64,c *int64,d int64) (S,*S,int64,int64,*int64,*int64) {
    6. var s1 = new(S)
    7. var s2 = new(S)
    8. var a = new(int64)
    9. *a = 10
    10. return *s1,s2,*a,*b,c,&d
    11. }
    12. func main(){}
    13. 输出:
    14. # command-line-arguments
    15. .\1.go:7:15: identity b does not escape
    16. .\1.go:7:24: leaking param: c
    17. .\1.go:7:33: moved to heap: d
    18. .\1.go:8:14: identity new(S) does not escape
    19. .\1.go:9:14: new(S) escapes to heap
    20. .\1.go:10:13: identity new(int64) does not escape
    21. 结果解释:
    22. identity b does not escape : 参数b传过来的是指针类型,且进行了*b操作,原本是要逃逸到堆的,但
    23. return时直接返回值,因此最终没发生逃逸,所以出现does not escape
    24. leaking param: c:参数c传进来,没做任何操作,直接返回,因此是leaking param,最终还是在栈上分配
    25. d :传进来时是值,return出去的是引用类型,因此逃逸
    26. var s1 = new(S) new操作一般是直接分配在堆上,但是在return的是*s1(值类型),因此没产生逃逸
    27. var s2 = new(S) new操作一般是直接分配在堆上,且return的是引用类,因此产生逃逸
    28. var a = new(int64) new操作一般是直接分配在堆上,但是在return的是*a(值类型),因此没产生逃逸

    例子3

    1. package main
    2. type slice []int64
    3. func identity() (slice,int64,*int64){
    4. var a = make([]int64,10)
    5. var b = make([]int64,10)
    6. var c = make([]int64,10)
    7. return a,b[0],&c[0]
    8. }
    9. func main(){}
    10. 输出:
    11. # command-line-arguments
    12. # command-line-arguments
    13. .\1.go:6:14: make([]int64, 10) escapes to heap
    14. .\1.go:7:14: identity make([]int64, 10) does not escape
    15. .\1.go:8:14: make([]int64, 10) escapes to heap
    16. 结果分析:
    17. var a = make([]int64,10) make操作一般是直接分配在堆上,且return的引用类型,因此产生逃逸
    18. var b = make([]int64,10) make操作一般是直接分配在堆上,但return时返回的是值类型,因此不逃逸
    19. var c = make([]int64,10) make操作一般是直接分配在堆上,且return的引用类型,因此产生逃逸

    例子4

    1. package main
    2. func identity() (interface{}){
    3. var s = "123"
    4. return s
    5. }
    6. func main(){}
    7. 输出:
    8. # command-line-arguments
    9. .\1.go:7:2: s escapes to heap
    10. 结果分析:
    11. 有一种特殊的情况需要考虑:interface类型,它是引用类型,无论何种情况,最终都会分配到堆上,
    12. 因为var s = "123" 原本是分配在栈上的,最终被分配到堆,因此会出现s escapes to heap

    tips:当变量过大,无论哪种情况,都会直接逃逸到堆