欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > golang 内存逃逸 栈与堆区别

golang 内存逃逸 栈与堆区别

2025/4/9 22:13:35 来源:https://blog.csdn.net/S_ZaiJiangHu/article/details/147018777  浏览:    关键词:golang 内存逃逸 栈与堆区别

介绍

我们要聊的就是“内存逃逸”——这个看起来很恐怖的名字其实说白了就是,某个变量的生命周期可能被 Go 的编译器不小心“推”到了堆上,而不是栈上,导致了一些不必要的内存消耗。那么,内存逃逸到底是怎么回事,怎么避免它呢?

面试应该从以下角度回答

  • 什么是逃逸?
  • 导致内存逃逸的原因是什么
  • 常见的发生逃逸的情况与逃逸分析
  • 如何避免

栈和堆

  • 栈(Stack):栈内存是程序运行时的一个局部区域,用于存储局部变量和函数调用的上下文。栈上的内存分配和释放速度非常快,因为栈是一个先进后出的结构,函数调用结束后,栈上的内存可以立即回收。
  • 堆(Heap):堆内存是动态分配的内存区域,通常用于存储生命周期不确定的对象,例如使用 new 或 make 创建的对象。堆内存的管理相对复杂,需要垃圾回收(GC)来进行清理,回收速度不如栈快。

什么是内存逃逸?

内存逃逸,顾名思义,就是程序在执行过程中,某些本应在栈上分配的变量,被“逃逸”到了堆上。为什么会发生这种情况呢?
在 Go 语言中,如果一个变量的生命周期不再局限于当前函数内,或者说,它的作用域扩展到了函数外面,Go 的编译器就会认为这个变量需要存储在堆上,而不是栈上。这就是所谓的“内存逃逸”。

为什么内存逃逸有问题?

栈内存的分配和释放是非常高效的,因此,程序员通常希望将变量保存在栈上,因为栈上的内存会随着函数的执行自动释放。然而,如果 Go 编译器无法优化内存分配,导致变量被错误地分配到了堆上,就会产生以下问题:

  • 内存开销增加:堆内存的分配和释放比栈慢,因为它依赖垃圾回收机制。而且堆内存的管理成本较高,会带来性能上的浪费。

  • GC负担加重:内存逃逸意味着更多的内存需要由垃圾回收(GC)来管理。如果大量的变量逃逸到堆上,GC 会变得更加繁重,可能会导致频繁的垃圾回收,影响程序的响应时间。

  • 不必要的内存消耗:有些变量本来只是局部变量,完全可以在栈上分配,结果却因为逃逸而浪费了堆内存。

  • 指针引用和内存安全

  • 内存泄漏风险

变量逃逸的常见原因

指针传递:

  • 分配在栈上:当原生类型被取地址且地址被赋值给了一个指针变量,当这个指针变量只是在函数内部使用,则这个原生类型会被分配在栈上(即使是通过new方法分配的)

  • 变量逃逸到堆上的情况:如果这个指针变量被以某种形式作为了函数返回值(例如,指针变量是struct中的变量,struct是函数返回值),则这个原生类型被分配在堆上(原因很简单,如果分配在栈上,函数返回后栈中的数据失效,这个指针指向的地址就是无效的)

如果你在函数内部定义了一个局部变量,并返回了这个变量的指针,那么这个变量就会逃逸到堆上,因为在函数返回后,这个指针可能会被外部代码访问。

func createSlice() *[]int {s := make([]int, 10)return &s // 错误:返回局部变量的地址
}正确的做法是直接返回切片本身,而不是其地址:func createSlice() []int {s := make([]int, 10)return s // 正确:返回切片的副本
}

如果你将一个局部变量的指针传递给了其他函数,Go 编译器会推测这个变量的生命周期已经超出了当前作用域,从而将它分配到堆上。

package mainimport "fmt"func foo(ptr *int) {fmt.Println(*ptr)
}func main() {x := 42foo(&x) // 这里传递了 x 的地址,x 会被分配到堆上
}

这个例子中,x 被传递给了 foo 函数,而 foo 是通过指针来访问 x 的。因为 Go
编译器无法确定 x 在 main 函数外部是否会继续使用,所以 x 被分配到了堆上

数组或切片的引用:

切片是一个动态数组,它分为

  • slice本身(即SliceHeader)
  • slice中的元素(即SliceHeader中Data)

与指针传递类似

  • 当返回指向slice的指针时,slice逃逸;
  • 当返回slice时,只有slice中的数据(Data)可能会逃逸(Data可能为地址)

具体

  • SliceHeader分配在栈上、Data分配在堆上
    当SliceHeader分配在栈上,Data既可以分配在栈上也可以分配在堆上

  • 当Data的空间不足、需要动态扩容时,Data会被分配在堆上
    当初始化slice时,Data所占空间达到64K时,SliceHeader和Data都会被分配在堆上(注意这里的64K边界是在自己的windows和linux机上测试到的,没有找go源码的出处,有可能不准确,理解为Data比较大时会直接分配在堆上比较好。另外除了slice,其他的数据类型如果初始化大小超过某个阈值时,应该也会直接分配在堆上)

  • 当SliceHeader分配在堆上,SliceHeader和Data都分配在堆上

代码示例:

package mainimport "fmt"func createSlice() []int {arr := [3]int{1, 2, 3} // arr 会逃逸到堆上return arr[:]
}func main() {slice := createSlice()fmt.Println(slice)
}

在上面的代码中,arr 是一个局部变量,而它被切片的返回值所引用。因为返回的是切片,而不是数组,所以 arr 会被分配到堆上。

map

1)不作为函数返回值时,分配在栈上
2)作为函数返回值且返回的不是指针时,map的元素分配在堆上,map本身分配在栈上
3)作为函数返回值且返回的是指针时,map的元素分配在堆上,map本身也分配在堆上

闭包(Closure):

闭包是导致内存逃逸的一个典型场景。因为闭包会持有外部函数的引用,所以即使外部函数已经返回,闭包内部的变量依然可能在堆上存活。

代码示例:

package mainimport "fmt"func main() {f := func() int {x := 42 // 这个变量 x 会逃逸到堆上return x}fmt.Println(f())
}

这里,x 是一个局部变量,但因为 f 是一个闭包,Go 编译器不能确定 x 的生命周期是否结束,所以 x 被分配到了堆上。

将局部变量的地址存储在全局变量或外部包中:

如果你将局部变量的地址存储在全局变量或外部包中,那么这个局部变量也会逃逸到堆上。

var globalPtr *intfunc setGlobalPtr() {local := 10globalPtr = &local // 错误:逃逸到堆上
}

正确的做法是使用指针指向一个新的堆分配的内存:

var globalPtr *intfunc setGlobalPtr() {local := 10localPtr := new(int) // 在堆上分配内存*localPtr = local    // 赋值给堆上的内存globalPtr = localPtr // 正确:globalPtr 指向堆内存
}

传递给包含指针的接口:

如果你将一个局部变量的地址传递给一个接口(特别是包含指针方法的接口),那么这个变量可能会逃逸。

type MyInterface interface {DoSomething()
}type MyStruct struct {value int
}func (m *MyStruct) DoSomething() {}func passToInterface(i MyInterface) {// i 可能持有对局部变量的引用,导致逃逸
}

如何避免内存逃逸?

为了提高程序的性能,减少内存消耗,避免内存逃逸是非常重要的。下面是一些常见的避免内存逃逸的技巧:

  • 尽量避免不必要的闭包:如果不是特别必要,尽量避免在函数内创建闭包,尤其是当闭包引用了大量外部变量时。闭包会把外部变量的生命周期拉长,导致这些变量被分配到堆上。

  • 避免指针传递:当传递变量的指针时,要尽量确保该变量的生命周期不会超出当前函数。如果能直接传值,而不是传指针,那就直接传值,避免不必要的堆分配。

  • 使用值传递而不是引用传递:对于小的、临时的结构体或数组类型,尽量采用值传递的方式,而不是传递其指针。这样,Go 编译器就可以安全地在栈上分配内存。

  • 避免返回切片或数组的引用:如果你只需要使用局部数据,尽量不要返回指向该数据的切片或数组。否则,它们的底层数组可能会逃逸到堆上。

如何检测内存逃逸?

Go 提供了 go run -gcflags=“-m” 命令来帮助我们检测内存逃逸问题。通过这个命令,我们可以查看编译器是否将某个变量分配到了堆上。

命令示例:

go run -gcflags="-m" main.go

当你运行这个命令时,Go 编译器会输出类似这样的信息:

/tmp/main.go:10:6: x escapes to heap

这表明变量 x 被分配到了堆上。

内存逃逸的优化

为了减少内存逃逸,可以通过优化代码来改善性能:

1)使用指针传递:将大的结构体或数组通过指针传递,避免复制数据。【需要修改原对象值,或占用内存比较大的结构体】
2)使用值传递:对于小的结构体或基本类型,可以使用值传递,避免指针的额外开销。【对于只读的占用内存较小的结构体】

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词