go逃逸分析
go 内存分配
go 内存详细过程参考这篇: go内存分配与垃圾回收
go 会在堆(heap, 全局的堆空间用来动态分配内存)和栈(每个 goroutine 的 stack)两个地方分配内存。
在函数中申请一个对象,如果分配在栈中,函数执行结束时自动回收,如果分配在堆中,则在函数结束后某个时间点进行垃圾回收。
在栈上分配和回收内存的开销很低,只需要 2 个 CPU 指令:PUSH 和 POP,一个是将数据 push 到栈空间以完成分配,pop 则是释放空间,也就是说在栈上分配内存,消耗的仅是将数据拷贝到内存的时间,而内存的 I/O 通常能够达到 30GB/s,因此在栈上分配内存效率是非常高的。
在堆上分配内存,一个很大的额外开销则是垃圾回收。Go 语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,提高了效率。
堆内存分配导致垃圾回收的开销远远大于栈空间分配与释放的开销。
逃逸分析
- 什么是内存逃逸?
内存逃逸(Memory Escape)是指Go语言中变量被分配到堆(Heap),而非栈(Stack)上的现象。
逃逸的核心原因:编译器无法确定变量的生命周期是否超出当前函数的作用域,因此将变量分配到堆上以确保其安全性。堆内存由垃圾回收器(GC)管理,而栈内存由编译器自动管理,生命周期受限于函数调用。
- 什么是逃逸分析?
Go 编译器怎么知道某个变量需要分配在栈上,还是堆上呢?编译器决定内存分配位置的方式,就称之为逃逸分析(escape analysis)。逃逸分析由编译器完成,作用于编译阶段。
可以通过 go build -gcflags=-m 查看逃逸分析情况, 输出类似:
1 | |
内存逃逸的例子
指针逃逸
1 | |
x是局部变量,函数返回其指针后,x的生命周期超出函数作用域,必须分配到堆上。
interface{} 动态类型逃逸
1 | |
接口类型包含指向具体数据的指针和类型信息,编译期无法确定具体类型, 赋值时需将数据分配到堆上。
闭包
1 | |
Increase() 返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。
内存逃逸的影响
- 性能开销:
- 堆分配速度慢:相比栈分配,堆分配需要更复杂的内存管理(如分配、碎片化处理)。
- GC压力增加:堆上分配的变量需要垃圾回收,频繁逃逸可能导致GC频繁运行,影响程序性能。
- 缓存效率降低:堆内存分散,可能导致CPU缓存命中率下降。
- 内存碎片化:
- 堆内存的动态分配和释放可能产生碎片,降低内存利用率。
- 潜在内存泄漏:
- 如果逃逸到堆的变量未被正确释放(如循环引用),可能导致内存泄漏。
go逃逸分析
https://haobin.work/2025/05/15/go/go逃逸分析/