``# Go 的堆栈与逃逸分析
堆栈
计算机中堆栈的区别

- 栈归 OS 分配和创建,堆由程序员使用语言来申请创建与释放
- 栈存储函数参数、返回值 、局部变量、函数调用时的临时上下文;堆存放全局变量
- 局部、占空间确定的数据放置在
Stack
上;否则放在堆
上(动态内存分配)
- 局部、占空间确定的数据放置在
- 栈的访问比堆快
- 每个线程分配一个栈,每个进程分配一个堆;
- stack 是线程独占的,heap 是线程共用的
- 栈创建时大小确定,超过数据存储则
stack overflow
,heap 大小可以动态增加 - 栈由高地址向低地址增长,堆由低地址向高地址增长
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
总结
- 在方法内把局部变量指针返回逃逸
- 局部变量本应该在栈上分配、回收,但是返回时被外部引用则生命周期大于栈
- 发送指针、带指针值到 channel 逃逸
- 编译时无法判断 goroutine 会在 channel 接收数据,则声明周期无法断定
- 切片存储指针/带指针的值 逃逸
[]*string
导致切片的内容逃逸,可能其背后的数组在栈上分配,但引用的值一定在heap 上
- slice 底层的数组被重新分配 逃逸
- slice 初始化时候会在栈上被分配,当其扩充时会在 heap 上分配,
append
时可能超出容量 cap
- slice 初始化时候会在栈上被分配,当其扩充时会在 heap 上分配,
- 在 interface{}类型上调用方法 逃逸
- interface{}上的方法调用都是动态的,方法的实现只有在运行时知道
``