如何判断变量是分配在栈(stack)上还是堆(heap)上?
Go和C++不同,Go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。判断语句:
// -m打印逃逸分析信息, -l禁止内联编译
go build -gcflags '-m -m -l' xxx.go
通常情况下:
栈上:函数调用的参数、返回值以及小类型局部变量大都会被分配到栈上,这部分内存会由编译器进行管理。无需GC标记
堆上:大对象、逃逸的变量会被分配到堆上。分配到堆上的对象,Go在运行时GC会在后台将对应的内存进行标记,从而能够在垃圾回收的时候将对应的内存回收,进而增加了开销。
例子:
func main() { a := make([]int, 10000) b := make([]int, 1000) print(a,b) }
可以看到
a 由于无法被一个执行栈装下,即使没有返回,也会直接在堆上分配;
b 能够被一个执行栈装下,变量没有返回到栈外,进而没有发生逃逸,分配到栈中
指针逃逸分析
局部变量以指针的方式从方法中传出或被全局变量引用,这种现象被称为指针逃逸(Escape).
情况一(最基本):在某个函数中new或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸
func main() {
c := call2()
print(c)
}
func call2() *int {
x := 2
return &x
}
情况二:指针作为函数调用参数,则该变量如果没有被逃逸的变量或者全局变量引用,指针不会逃逸。
func main() {
a := make([]int, 5)
call(&a)
}
func call(a *[]int) {
(*a)[0] = 1
}
情况三:仅在函数内对变量做取值操作,而未将指针传出,指针不会逃逸
func main() {
a := 2
b := &a
print(b)
情况四:指针作为函数调用参数,则该变量如果被逃逸的变量或者全局变量引用,指针会逃逸
var g *int
func main() {
a := 2
g = &a
}
情况五:被指针类型的slice、map和chan引用的指针一定会发生逃逸
func main() {
a := make([]*int, 1)
b := 12
a[0] = &b
c := make(map[string]*int)
d := 14
c["aaa"] = &d
e := make(chan *int, 1)
f := 15
e <- &f
}
总结
golang 变量存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。非指针小对象通常保存在栈上,大对象保存在堆上。至于指针保存在堆上还是栈上,要进行逃逸分析:
指针必然发生逃逸的三种情况:
- 在某个函数中new或字面量创建出的变量,将其指针作为函数返回值,则该变量一定发生逃逸(构造函数返回的指针变量一定逃逸);
- 被已经逃逸的变量引用的指针,一定发生逃逸;
- 被指针类型的slice、map和chan引用的指针,一定发生逃逸;
一些必然不会逃逸的情况:
- 指针被未发生逃逸的变量引用;
- 仅仅在函数内对变量做取址操作,而未将指针传出;