go逃逸分析

go 内存分配

go 内存详细过程参考这篇: go内存分配与垃圾回收

go 会在堆(heap, 全局的堆空间用来动态分配内存)和栈(每个 goroutine 的 stack)两个地方分配内存。
在函数中申请一个对象,如果分配在栈中,函数执行结束时自动回收,如果分配在堆中,则在函数结束后某个时间点进行垃圾回收。

在栈上分配和回收内存的开销很低,只需要 2 个 CPU 指令:PUSH 和 POP,一个是将数据 push 到栈空间以完成分配,pop 则是释放空间,也就是说在栈上分配内存,消耗的仅是将数据拷贝到内存的时间,而内存的 I/O 通常能够达到 30GB/s,因此在栈上分配内存效率是非常高的。

在堆上分配内存,一个很大的额外开销则是垃圾回收。Go 语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,提高了效率。
堆内存分配导致垃圾回收的开销远远大于栈空间分配与释放的开销。

逃逸分析

  1. 什么是内存逃逸?
    内存逃逸(Memory Escape)是指Go语言中变量被分配到堆(Heap),而非栈(Stack)上的现象。

逃逸的核心原因:编译器无法确定变量的生命周期是否超出当前函数的作用域,因此将变量分配到堆上以确保其安全性。堆内存由垃圾回收器(GC)管理,而栈内存由编译器自动管理,生命周期受限于函数调用。

  1. 什么是逃逸分析?
    Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。

可以通过 go build -gcflags=-m 查看逃逸分析情况, 输出类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go build -gcflags=-m main.go
# command-line-arguments
./main.go:7:6: can inline foo
./main.go:13:10: inlining call to foo
./main.go:16:13: inlining call to fmt.Println
/var/folders/45/qx9lfw2s2zzgvhzg3mtzkwzc0000gn/T/go-build409982591/b001/_gomod_.go:6:6: can inline init.0
./main.go:7:10: leaking param: s
./main.go:8:10: new(A) escapes to heap
./main.go:16:13: io.Writer(os.Stdout) escapes to heap
./main.go:16:13: c escapes to heap
./main.go:15:9: b + "!" escapes to heap
./main.go:13:10: main new(A) does not escape
./main.go:14:11: main a.s + " world" does not escape
./main.go:16:13: main []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape

内存逃逸的例子

指针逃逸

1
2
3
4
func createPointer() *int {
x := 10
return &x // x 逃逸到堆
}

x是局部变量,函数返回其指针后,x的生命周期超出函数作用域,必须分配到堆上。

interface{} 动态类型逃逸

1
2
3
4
func interfaceEscape() interface{} {
x := 10
return x // x 逃逸到堆
}

接口类型包含指向具体数据的指针和类型信息,编译期无法确定具体类型, 赋值时需将数据分配到堆上。

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}

func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}

Increase() 返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。

内存逃逸的影响

  • 性能开销:
    • 堆分配速度慢:相比栈分配,堆分配需要更复杂的内存管理(如分配、碎片化处理)。
    • GC压力增加:堆上分配的变量需要垃圾回收,频繁逃逸可能导致GC频繁运行,影响程序性能。
    • 缓存效率降低:堆内存分散,可能导致CPU缓存命中率下降。
    • 内存碎片化:
    • 堆内存的动态分配和释放可能产生碎片,降低内存利用率。
  • 潜在内存泄漏:
    • 如果逃逸到堆的变量未被正确释放(如循环引用),可能导致内存泄漏。

go逃逸分析
https://haobin.work/2025/05/15/go/go逃逸分析/
作者
Leo Hao
发布于
2025年5月15日
许可协议