堆内存与栈内存
程序会在两个地方为变量分配内存,一个是全局的堆空间用来动态分配内存,另一个是goroutine的栈空间,Go语言拥有自动的垃圾回收机制,我们不需要关心内存到底是分配在堆上还是栈上,但是从性能的角度出发,分配在栈空间还是堆空间差异较大。
在函数中申请一个对象,如果分配在栈上,函数执行完毕自动回收,如果分配在堆上,则在函数执行完毕后某个时间点进行垃圾回收。
在栈上分配或者回收内存的开销很低,只需要两个CPU指令,一个是Push,一个是Pop,Push代表分配内存,Pop代表释放内存。在栈上分配内存,消耗的仅是将内存分配到栈上的时间,内存的IO速度通常能够达到30GB/s,因此在栈上分配内存效率比较高。
在堆上分配内存,一个很大的额外开销是垃圾回收,Go语言使用的是清除标记算法,并在此基础上,使用了三色标记法和写屏障算法,提高了效率。
逃逸分析
Go语言中,堆内存是通过垃圾回收机制自动管理的,那么Go的编译器如何知道内存是分配在栈上还是堆上呢?编译器决定内存分配位置的方式,就叫逃逸分析。逃逸分析由编译器完成,作用于编译阶段。
指针逃逸
在函数中创建了一个对象,返回了这个对象的指针。这种情况下,函数虽然退出了,但是因为指针的存在,对象的内存不能随着函数的结束而回收,因此只能分配在堆上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
type Demo struct {
name string
}
func createDemo(name string) *Demo {
d := new(Demo)
d.name = name
return d
}
func main() {
demo := createDemo("demo")
fmt.Println(demo)
}
|
1
|
go run -gcflags "-m -l" Demo30.go
|
1
2
3
4
|
./Demo30.go:9:17: leaking param: name
./Demo30.go:10:10: new(Demo) escapes to heap
./Demo30.go:16:13: ... argument does not escape
&{demo}
|
new(Demo) escapes to heap说明new(Demo)逃逸到了堆上
Interface动态类型逃逸
空接口Interface{}可以表示任何类型,如果函数的参数为interface{},编译期无法确定具体类型,则会发生逃逸。
在上面的例子中
1
2
3
4
|
func main() {
demo := createDemo("demo")
fmt.Println(demo)
}
|
我们将main函数中的局部变量demo作为实际参数传给fmt.Println方法,
fmt.Println方法的实现如下
1
2
3
|
func Println(a ...any) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
|
参数类型为a…any即interface{}类型,因此发生了逃逸。
栈空间不足
超过一定大小的局部变量会逃逸到堆上,变量大小不确定,也会逃逸到堆上。
闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import "fmt"
func Increment() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
increment := Increment()
fmt.Println(increment())
fmt.Println(increment())
}
|
Increment()返回值是一个闭包函数,该匿名函数访问了外部变量n,外部变量n将一直存在,直到increment被销毁。变量n的内存不能随着函数Increment()推出而被销毁,因此将会逃逸到堆上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
➜ Demo go run -gcflags=-m Demo31.go
# command-line-arguments
./Demo31.go:5:6: can inline Increment
./Demo31.go:7:9: can inline Increment.func1
./Demo31.go:14:24: inlining call to Increment
./Demo31.go:7:9: can inline main.func1
./Demo31.go:15:23: inlining call to main.func1
./Demo31.go:15:13: inlining call to fmt.Println
./Demo31.go:16:23: inlining call to main.func1
./Demo31.go:16:13: inlining call to fmt.Println
./Demo31.go:6:2: moved to heap: n
./Demo31.go:7:9: func literal escapes to heap
./Demo31.go:14:24: func literal does not escape
./Demo31.go:15:13: ... argument does not escape
./Demo31.go:15:23: ~R0 escapes to heap
./Demo31.go:16:13: ... argument does not escape
./Demo31.go:16:23: ~R0 escapes to heap
|
一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能