前言

在编程领域中,理解和管理内存是提高代码性能和稳定性的关键。尤其在 Go 语言中,一个强大的垃圾收集器和内存模型使得内存管理变得相对直观,但这并不意味着我们可以完全忽视内存的使用。其中,内存逃逸是一个值得我们深入理解的概念,因为它直接影响到变量的生命周期和垃圾收集的效率。

在这篇文章中,我将详细地探讨 Go 语言中的内存逃逸,以及内存逃逸将造成什么影响,如何分析并优化内存逃逸现象。

概念

在 C/C++ 开发中,需要开发者手动分配内存(malloc/new),使用完后的内存需要手动回收(delete)。这样做的好处是能够细致掌握内存的分配,但因此也会降低开发者的开发效率,而且若是忘记回收内存,就会造成内存泄露。因此,大多数的高级语言都实现了垃圾回收机制(GC),通过编译器来管理内存分配。

go 语言中,在函数中创建一个局部变量,编译器在编译的时候就会根据一定的规则,指定该变量的内存是分配到栈还是分配到堆。而对于分配到堆上的内存,就称之为内存逃逸。

内存分配规则

一般情况下,在函数中创建的变量都应该分配到栈上,以下情况才会分配到堆上,即内存逃逸:

  1. 创建的局部变量通过返回值返回了其指针,该变量可能会在函数外被访问
  2. 创建的变量需要占用很大的内存(栈空间不足逃逸)
  3. 函数闭包中引用的局部变量
  4. 接口动态分配,即函数参数为接口类型,传入的参数为接口的实现类型

简单来说,就是可能会被外部引用的局部变量,编译器就会将其分配到堆上。

内存逃逸的常见场景

  1. 在函数返回值中返回了指针类型
1
2
3
4
func (s *Service) Method(ctx context.Context, input *MethodInput) (*MethodOutput, error) {
res := &GetProgressOutput{}
return res, nil
}
  1. 变量内存占用太大
1
2
3
4
5
6
7
func main() {
s1 := make([]int, 1000, 1000)
s1[0] = 0

s2 := make([]int, 10000, 10000)
s2[0] = 0
}
1
2
3
.../main.go:3:6: can inline main
.../main.go:4:12: make([]int, 1000, 1000) does not escape
.../main.go:7:12: make([]int, 10000, 10000) escapes to heap
  1. 闭包引用了局部变量
1
2
3
4
5
6
7
8
9
func main() {
f1 := func() func() {
a := 1
return func() {
a++
}
}
f1()
}
1
.../main.go:5:3: moved to heap: a
  1. 动态类型参数
1
2
3
func main() {
fmt.Println(1) // func Println(a ...any) (n int, err error)
}
1
.../main.go:6:14: 1 escapes to heap

栈分配和堆分配的区别

栈分配

栈分配的速度非常快,只需要两个 CPU 指令:“PUSH”和“RELEASE”,分别用来分配和释放内存。

分配到栈上的数据,访问速度比分配到堆上的快,在函数返回的时候,就会自动释放所占用的栈空间。

堆分配

堆分配时首先要去内存中找到一块大小合适的内存块,因此开销会比栈分配大许多。

堆上的内存不会在函数结束的时候自动释放,需要通过 GC 回收。

内存逃逸的影响

  1. 堆上的内存只能通过 GC 回收,因此程序会占用更多的内存空间
  2. 堆分配需要更多的资源开销,会导致服务性能下降
  3. 内存逃逸太多的话会频繁GC,导致服务稳定性下降

内存逃逸分析

可以在编译程序的时候通过 -gcflags="-m" 选项来检测内存逃逸,编译器会输出哪些变量发生了逃逸。

1
go build -gcflags="-m" main.go
1
2
3
cmd\api\main.go:72:12: ... argument escapes to heap
cmd\api\main.go:79:12: ... argument escapes to heap
cmd\api\main.go:85:12: ... argument escapes to heap

总结

通常情况下,内存逃逸是一个正常的编程现象,大多数开发者并不需要过分关注。这是因为 Go 语言的垃圾回收机制(GC)会自动管理和回收堆内存,使得开发者可以更专注于编程逻辑而非内存管理。

然而,对于那些对性能有特别要求的应用来说,理解并优化内存逃逸是非常必要的。内存逃逸的发生意味着更多的堆分配和垃圾回收操作,这可能会对程序性能产生影响。因此,对内存逃逸进行深入分析,并尽可能减少它的发生,通过降低垃圾回收的频率和堆内存的分配,可以有效地提升程序的性能。

总的来说,虽然内存逃逸在日常编程中可能不需要过分关注,但在追求高性能的场景下,优化内存逃逸是一种重要的性能优化手段。