``# Go 的堆栈与逃逸分析

堆栈

计算机中堆栈的区别

UvOwW0
  1. 栈归 OS 分配和创建,堆由程序员使用语言来申请创建与释放
  2. 栈存储函数参数、返回值 、局部变量、函数调用时的临时上下文;堆存放全局变量
    1. 局部、占空间确定的数据放置在Stack上;否则放在上(动态内存分配)
  3. 栈的访问比堆快
  4. 每个线程分配一个,每个进程分配一个
    1. stack 是线程独占的,heap 是线程共用的
  5. 栈创建时大小确定,超过数据存储则stack overflow,heap 大小可以动态增加
  6. 栈由高地址向低地址增长,堆由低地址向高地址增长

Go 的堆栈

变量存储在 heap 还是 stack 由编译器决定

  • 无法证明函数返回后变量是否被引用则必须在 heap 上分配,避免指针悬空
  • 局部变量过大也会分配在 heap 上
  • 变量具有地址,作为堆分配的候选,逃逸分析确定其生存周期不会超过函数返回,就被分配在栈上

go tool compile命令查看汇编判断存储位置

示例:

package main
import "fmt"
func main() {
	var a [1]int
	c := a[:]
	fmt.Println(c)
}

go tool compile -m heapAndStackAnalyse.go显示

heapAndStackAnalyse.go:8:13: inlining call to fmt.Println
heapAndStackAnalyse.go:6:6: moved to heap: a
heapAndStackAnalyse.go:8:13: c escapes to heap
heapAndStackAnalyse.go:8:13: []interface {}{...} does not escape
<autogenerated>:1: .this does not escape

Go 逃逸分析

Go 中的变量存储分配由编译器决定;生命周期延长则会分配到heap发生逃逸

**定义:**编译器通过分析自动判断变量的生命周期是否被延长,判断过程即为逃逸分析

上述代码fmt.Println(c) 发现调用fmt包的Println函数 ,扩展了生命周期使得 c escape to heap

改为 Println(c)则不会发生逃逸

Go 的调用栈

了解 Go 调试时追踪堆栈的跟踪信息和识别传递的参数

示例:

package main
import "runtime/debug"

func main() {
	slice := make([]string, 2, 4)
	Example(slice, "hello", 10)
}
func Example(slice []string, str string, i int) {
	debug.PrintStack()
}

Go 中程序的启动使用 goroutine 下述第一行表明,goroutineID=1;

后续为不同层次的调用

  • 最深层调用最先打印,最后打印最浅调用。
goroutine 1 [running]:
runtime/debug.Stack(0xc000046778, 0xc000070f78, 0x1004685)
        /usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:24 +0x9f
runtime/debug.PrintStack()
        /usr/local/Cellar/go/1.16.6/libexec/src/runtime/debug/stack.go:16 +0x25
main.Example(...)
        //...go:11
main.main()
        //....go:7 +0x25

总结

  1. 在方法内把局部变量指针返回逃逸
    • 局部变量本应该在栈上分配、回收,但是返回时被外部引用则生命周期大于栈
  2. 发送指针、带指针值到 channel 逃逸
    • 编译时无法判断 goroutine 会在 channel 接收数据,则声明周期无法断定
  3. 切片存储指针/带指针的值 逃逸
    • []*string导致切片的内容逃逸,可能其背后的数组在栈上分配,但引用的值一定在heap 上
  4. slice 底层的数组被重新分配 逃逸
    • slice 初始化时候会在栈上被分配,当其扩充时会在 heap 上分配,append时可能超出容量 cap
  5. 在 interface{}类型上调用方法 逃逸
    • interface{}上的方法调用都是动态的,方法的实现只有在运行时知道

``