在理解内存逃逸之前,我们需要先了解下啥是堆内存和栈内存
堆和栈
栈内存:由编译器自动管理,自动分配管理,存放局部变量,函数参数等
堆内存:一般需要人为手动管理,手动申请、分配和释放等,在c/c++中,是需要通过new/malloc进行动态分配内存,而golang中,一般是垃圾回收进行处理。
优缺点:
栈内存分配快速,一般退出函数后就自动回收了,开销比较小,栈内存不需要gc参与
堆内存分配代价大,在golang中,堆内存的回收需要垃圾回收(也就是gc)进行参与,而垃圾回收则是需要比较大的系统开销。
内存逃逸
在编写golang程序代码时,某些变量和数据的生命周期超出了原始作用域的情况就称为内存逃逸。当内存逃逸时,变量和数据内存会从栈区逃逸到堆区。
逃逸的情况
1、变量超出作用域:
在函数内声明的变量,如果在函数返回时仍然被引用,就会导致内存逃逸。分配在堆区,保证函数返回时继续可以用
2、引用外部变量
在函数内部引用了外部变量,可能导致内存逃逸。编译器会无法确定变量的生命周期,可能分配到堆区
3、使用闭包
闭包可以捕获外部变量值,这些变量的生命周期可能超出了闭包本身的生命周期
检测方法
go build增加 -gcflags="-m -l",如下
go build -gcflags="-m -l" test.go
如下显示表示逃逸到了堆区
demo样例
1、函数返回继续引用变量
NewImCache函数返回之后,局部变量c仍然被引用,因此逃逸到了堆区
package main
import "fmt"
var cache = NewImCache()
func NewImCache() *imCache {
c := new(imCache)
return c
}
func (i *imCache) GetNum() int {
return 1
}
type imCache struct {
}
func main() {
res := cache.GetNum()
fmt.Print(res)
}
2、闭包捕获
闭包函数内部捕获了外部变量count。但是闭包函数的生命周期超出了包含它的函数,所以逃逸到堆区
package main
import "fmt"
func main() {
res := counter()
fmt.Println(res())
}
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
3、使用协程
协程中引用了外部变量res,导致data逃逸到了堆区
package main
import "fmt"
func main() {
res := 1
go func() {
fmt.Println(res)
}()
}