一文详解Go内存分配与逃逸分析
前言
逃逸分析,简单的说就是确定内存是分配到栈上还是堆上。
将内存分配到栈上的性能会比分配到堆上的性能明显要高,Go在编译阶段就会完成逃逸分析,编译器会尽可能的将内存分配到栈上,但当检查到变量在生命周期内发生未知的情况,则会产生逃逸现象,从而会将变量的内存分配到堆上。
插曲:为什么将内存分配到栈上的性能会比分配到堆上的性能明显要高呢?
原因是因为栈的内存是由编译器自动进行分配和释放的,非常高效,它们随着函数的创建而分配,随着函数的退出而销毁;而对于堆上的内存回收,得通过Go的三色标记法,进行垃圾回收,并且有时还需要加锁来防止多线程冲突。
逃逸分析原则
- 编译阶段无法确定的参数,会逃逸到堆上;
- 变量在函数外部存在引用,会逃逸到堆上;不存在引用,则会继续在栈上;
- 变量占用内存较大时,会逃逸到堆上;
逃逸分析举例
下面通过举例,来进一步论证逃逸分析的原则,加深一下理解
我们可以使用这个命令go build -gcflags '-m -m -l' go文件名
,来查看逃逸分析的结果
1.参数为interface类型会逃逸
func main() {
num := 1
fmt.Println(num)
}
运行结果:
原因分析:
func Println(a ...interface{}) (n int, err error)
,这个函数的入参是interface类型
,编译阶段无法确定其具体的参数类型,所以内存分配到堆上
2.变量在函数外部有引用会逃逸
func main() {
_ = test()
}
func test() *int {
num := 10
return &num
}
运行结果:
原因分析:
变量num在函数外部存在引用,函数退出时栈中的内存(栈帧)已经释放,但引用已经被返回,如果通过引用地址取值,在栈中是取不到值的,所以Go为了避免这个情况,会将内存分配到堆上
3.变量占用内存较大时会逃逸
func main() {
//不会逃逸
s1 := make([]int, 10, 10)
for i := 0; i < 10; i++ {
s1[i] = i
}
//会逃逸
s2 := make([]int, 10000, 10000)
for i := 0; i < 10000; i++ {
s2[i] = i
}
}
运行结果:
原因分析:
切片容量过大时,会产生逃逸,内存分配到堆上;容量小时,不会逃逸,内存分配依赖在栈上
4.变量大小不确定时会逃逸
func main() {
num := 10
s := make([]int, num, num)
for i := 0; i < num; i++ {
s[i] = i
}
}
运行结果:
原因分析:
切片的长度和容量,虽然通过声明的变量num来指定了,但在编译阶段是未知的,并不确定num的具体值,所以会逃逸,将内存分配到堆上
应用实践
通过上面的举例,更加清晰的知道了哪些情况会产生逃逸,哪些不会产生逃逸。知道了这些,才能写出更优质的代码。
实践案例:方法接收者变量,什么时候用结构体,什么时候用结构体指针呢?
分情况讨论:
- 结构体较大时:如果使用结构体,虽然分配在栈上不逃逸,但会产生值拷贝,带来的是更多的性能开销与内存占用,所以使用结构体指针更加合适,产生了逃逸但节省了内存空间,同时也避免了额外的性能开销;
- 结构体较小时:这种情况就适合使用结构体,分配在栈上,减少了GC压力的同时提高了性能;
总结
内存分配到栈上还是堆上,通过逃逸分析就能明确知道。基于逃逸分析的原则,使用举例来做了论证,加深了理解与使用。
弄清楚了逃逸分析,结合实际应用场景,我们才能写出更优质的代码,要尽可能的让内存分配到栈上,减少GC压力以提高性能,但并不绝对,额外的性能开销与内存空间的高占用,我们也得选择更适当的平衡点。
如果本文对你有帮助,欢迎点赞收藏加关注,如果本文有错误的地方,欢迎指出!
转载自:https://juejin.cn/post/7205903144173486138