Go - 内存逃逸

概念

每个函数都有自己的内存区域来存放自己的局部变量、返回地址等,这个内存区域在栈中进行分配。当函数结束时,这段内存区域会进行释放。
但有些变量,我们想在函数结束后仍然使用它,那么就要把这个变量在堆上分配,这种从“栈”上逃逸到“堆”上的现象就是内存逃逸。

在栈上分配的内存,由系统申请和释放,不会有额外的性能开销。
而在堆上分配的内存,如果要回收掉,就需要进行GC,内存逃逸额外带来的GC会导致性能开销变大。

逃逸机制

根据变量是否被外部引用来决定是否逃逸:

  1. 若函数外部没有引用,则优先放入栈中
  2. 若函数外部存在引用,则优先放入堆中
  3. 若栈上放不下,则必定放到堆中

逃逸分析

通过编译参数-gcflags=-m查看编译过程中的逃逸分析。

一般的逃逸类型:

指针逃逸

在函数中创建了一个对象,返回了这个对象的指针。此时,函数虽然推出了,但因为指针的存在,对象的内存不能随着函数结束而回收,只能逃逸到堆上。

package main  
import "fmt"  
type Demo struct {  
	name string  
}  
func createDemo(name string) *Demo {  
	d := new(Demo) // 局部变量 d 逃逸到堆  
	d.name = name  
	return d  
}  
func main() {  
	demo := createDemo("demo")  
	fmt.Println(demo)  
}

interface{}动态类型逃逸

如果函数的参数为interface{}类型,编译期间很难确定其参数具体类型,也会发生逃逸。

func main() {  
	demo := "demo" 
	fmt.Println(demo)  
}

demo作为实参传给Println(i interface{}) (n int, err error)方法,因为该函数的参数类型定义为interface{},因此会发生逃逸。

栈内存不足

操作系统对内核线程使用的栈空间是有大小限制的,因为栈空间通常较小,因此递归函数实现不当时,容易导致栈溢出。
对于Go语言来说,运行时(runtime)尝试在goroutine需要的时候动态地分配栈空间,goroutine的初始栈大小为2kb。
当goroutine被调度时,会绑定到内核线程执行,所以栈空间大小也不会炒股共操作系统的限制。

对于Go编译器来说,超过一定大小的局部变量将逃逸到堆上。

// 1.
func generate8192() {  
	nums := make([]int, 8192) // = 64KB  
	for i := 0; i < 8192; i++ {  
		nums[i] = rand.Int()  
	}  
}  
// 2.
func generate8193() {  
	nums := make([]int, 8193) // > 64KB  
	for i := 0; i < 8193; i++ {  
		nums[i] = rand.Int()  
	}  
}  
// 3.
func generate(n int) {  
	nums := make([]int, n) // 不确定大小  
	for i := 0; i < n; i++ {  
		nums[i] = rand.Int()  
	}  
}  
  
func main() {  
	generate8192()  
    generate8193()  
    generate(1)  
}

2和3均逃逸到堆上,而1没有。
说明当切片内存超过一定大小,栈空间不足时,便会逃逸到堆上。

闭包

当闭包访问到其外层的函数作用域时,会发生内存逃逸。

func Increase() func() int {  
	n := 0  
	return func() int {  
		n++  
		return n  
	}  
}  
  
func main() {  
	in := Increase()  
	fmt.Println(in()) 
}

Increase()返回值是一个闭包函数,该闭包函数引用了外部变量n,知道in()被销毁,n却不能随着函数退出而被回收,因此逃逸到堆上。

内存逃逸的影响

在栈上分配和回收内存的开销是很低的,只需要poppush命令,分别负责释放栈空间和分配数据内存。在栈上分配内存,消耗的仅是将数据拷贝到内存的时间。

而在堆上分配内存,很大的额外开销是垃圾回收。

如果频繁发生内存逃逸,会导致程序占用过多的内存资源,影响程序的性能和稳定性。主要体现在以下几个方面:

  • 内存占用增加:由于堆分配的内存不会自动释放,所以会导致程序占用的内存资源不断增加,特别是在长时间运行的程序中,可能会导致系统资源耗尽。
  • 性能下降:相比于栈分配,堆分配需要更多的 CPU 和内存资源,stw,因此会导致程序的运行速度变慢。
  • 程序不稳定:如果程序中存在大量的内存逃逸,可能会导致垃圾回收器频繁工作,从而影响程序的稳定性。
### Golang 中的内存逃逸分析与优化 #### 内存逃逸的概念 在 Go 语言中,内存逃逸是指当一个局部变量被分配到堆上而不是栈上的情况。通常情况下,局部变量会被分配在栈上以获得更好的性能,因为栈的操作速度远快于堆。然而,在某些特定条件下,编译器会决定将这些局部变量移到堆上来确保程序的正确性和安全性[^1]。 #### 导致内存逃逸的原因 多个因素可能导致内存逃逸的发生: - **返回指针或引用**:如果函数内部创建的对象需要在其作用域外仍然有效,则该对象必须位于堆上。 - **闭包捕获变量**:当匿名函数捕捉外部范围内的变量时,为了保证其生命周期超过当前执行上下文,这些被捕获的变量可能会被移动至堆上。 - **大尺寸的数据结构**:对于非常大的复合类型实例(如数组),即使它们只存在于本地范围内也可能放置在堆上以便更好地管理内存布局。 - **接口转换**:任何涉及接口类型的赋值操作都可能触发逃逸行为,尤其是当具体实现不是内联的小型结构体时[^2]。 #### 如何避免不必要的内存逃逸 为了避免不必要地增加堆分配次数并提升应用程序的整体效率,开发人员可以从以下几个方面着手改进代码设计: - **优化数据结构的选择**:尽量采用简单而紧凑的形式表示所需的信息;减少复杂嵌套层次以及动态大小字段的数量。 - **优先考虑传值而非传递指针**:除非确实有必要共享状态或者处理大型实体,否则应倾向于按值复制参数来保持独立副本之间的隔离性。 - **限制全局/静态存储区使用频率**:过度依赖此类区域容易造成难以追踪的状态变化,并且不利于并发场景下的同步控制。 - **谨慎对待接口和反射机制的应用**:虽然二者提供了极大的灵活性,但也增加了潜在的风险点——特别是有关运行期类型检查开销及间接寻址带来的额外负担。 - **审查递归算法的设计合理性**:深层数量级较大的迭代过程往往伴随着较高的空间需求增长趋势,需评估是否有更优解法替代传统方式[^3]。 下面是一个简单的例子展示了如何通过调整代码逻辑降低发生逃逸的可能性: ```go // 不推荐的做法:此版本中的切片s将会逃逸到heap上 func badExample() []int { var s []int for i := 0; i < 1e6; i++ { s = append(s, i) } return s } // 推荐做法:预先声明容量可防止频繁扩容引起逃逸 func goodExample() []int { s := make([]int, 0, 1e6) // 预先指定足够的长度 for i := 0; i < 1e6; i++ { s = append(s, i) } return s } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值