楔子
对于像 C 这样的语言, 不同位置的变量应该申请在内存的哪个区都是很固定的, 比如: 全局变量会在全局区创建, 函数里面的变量会在栈区创建, 并且我们还可以手动地从堆区申请内存, 手动地释放内存. 但是到了 golang 中, 这些都不需要我们管了, 我们不需要关心变量到底申请在哪个区, 编译器的垃圾回收机制会自动帮我们判断创建的变量到底应该分配到哪个区. 可是 golang 的编译器是如何得知的呢? 比如这样一个例子:
- func sum(a int, b int) *int {
- var c = a + b
- return &c
- }
这段代码表示接收两个整型, 然后相加用变量存储起来, 返回变量的指针. 这段代码在 C 程序员的眼中肯定是有问题的, 但是在 golang 中这是完全正常的代码. 因为这个变量 c 是分配在堆上的, 如果我们返回的不是 c 的指针, 而是 c, 那么 c 这个变量就会分配在栈上面, 所以我们看到一个变量究竟分配在什么地方, golang 会帮我们进行检测. 如果返回一个值的话 (golang 是值拷贝), 那么原来的变量 c 对应的内存就会被回收, 如果返回的是指针 (&c), 那么 c 对应的内存就不会被回收, 因为 golang 知道要是回收了, 那么返回的指针就获取不到指向的值了, 于是就会把 c 分配到堆上. 那么 golang 是如何检测的呢, 答案是通过逃逸分析.
什么是逃逸分析?
逃逸分析: 通过指针的动态范围决定一个变量究竟是分配在栈上还是应该分配在堆上.
我们知道栈区是可以自动清理的, 所以栈区的效率很高, 但是不可能把所有的对象都申请在栈上面, 而且栈空间也是有限的. 但如果所有的对象都分配在堆区的话, 堆又不像栈那样可以自动清理, 因此会频繁造成 golang 的垃圾回收, 从而降低运行效率. 这里多提一句, python 中的对象都是分配在堆上的, python 中的对象本质上就是 c 中 malloc 在堆上申请的一块内存, 尽管 python 通过内存池降低了频繁和操作系统交互, 但还是架不住效率低. 所以在 golang 中, 会通过逃逸分析, 把那些一次性的对象分配到栈区, 如果后续还有变量指向, 那么就放到堆区.
逃逸分析的标准
首先可以肯定的是, 如果函数里面的变量返回了一个地址, 那么这个变量肯定会发生逃逸. golang 的编译器会来进行判断变量的生命周期, 如果编译器认为函数结束后, 这个变量不再被外部的引用了, 会分配到栈, 否则分配到堆.
所以变量分配到栈还是分配到堆, 不是我们能决定的, 而是编译器经过分析之后得出的.
- func sum(a int, b int) *int {
- var c = a + b
- var d = 1
- var e = new(int)
- fmt.Println(&d)
- fmt.Println(&e)
- return &c
- }
- // 比如这里的变量 d, 尽管通过 & d 获取了它的地址, 但是这仅仅是打印.
- // 而 e 虽然调用了 new 方法, 但这并不能成为分配到堆区的理由
- // 因为外部并没有人引用 d 和 e, 所以不好意思, sum 函数执行结束, 这两位老铁必须 "见上帝"
- // 但是对于 c, 我们返回了它的指针, 既然返回了指针, 那么就代表它指向的内存可以被外部访问, 所以会逃逸到堆
如果一个函数结束之后外部没有引用, 那么优先分配到栈中 (如果申请的内存过大, 栈区存不下, 会分配到堆).
如果一个函数结束之后外部还有引用, 那么必定分配到堆中
逃逸分析演示
- package main
- import "fmt"
- func sum(a int, b int) *int {
- var c = a + b
- return &c
- }
- func main() {
- var p = sum(1, 2)
- fmt.Println(p)
- }
通过命令 go build -gcflags "-m -l" xxx.go 观察 golang 是如何进行逃逸分析的
- # command-line-arguments
- .\1.go:7:9: &c escapes to heap
- .\1.go:6:6: moved to heap: c
- .\1.go:12:13: p escapes to heap
- .\1.go:12:13: main ... argument does not escape
我们看到变量 c 发生了逃逸, 这和我们想的一样, 但是为什么 main 函数里面的 p 居然也逃逸了. 因为编译期间不确定变量类型的话, 那么也会发生逃逸.
除此之外, 还可以通过反汇编命令来查看, go tool compile -S xxx.go
总结
堆上动态内存分配的开销比栈要大很多, 所以有时我们传递值比传递指针更有效率. 因为复制是栈上完成的操作, 开销要比变量逃逸到堆上再分配内存要少的多, 比如说:
- func func1(a int, b int) *int {
- var c = a + b
- func2(&c)
- }
- func func2(p *int){
- }
因为 func2 里面接收一个指针, 所以 func1 里面的 c 变量毫无疑问会逃逸到堆, 在堆上分配, 而如果 func2 接收不是指针, 那么 c 变量会直接栈上分配, 然后只需要拷贝一个值即可, 因为是栈, 所以拷贝值的效率反而会更高一些. 因为逃逸到堆, 再分配的效率更低. 因此根据场景具体分析, 到底函数要不要接受指针, 总之擅用 golang 的逃逸分析, 当然我们不需要知道编译器的逃逸分析规则, 只需要观察程序的运行情况就行了.
来源: http://www.bubuko.com/infodetail-3384911.html