什么是逃逸?
逃逸是指一个变量本来应该分配在栈(stack)上,但由于某些原因,最终被分配到了堆(heap)上。
类比:
- 栈就像一个临时的快餐盒,用来存放短期使用的数据。
- 堆就像一个长期的仓库,用来存放需要长期存在的数据。
- 如果快餐盒里的东西需要长期保存,就会被放到仓库里,这就是逃逸。
为什么会出现逃逸?
Go 的逃逸分析会决定变量是分配在栈上还是堆上。逃逸分析的准则是:
-
指向栈对象的指针不能存在堆上
- 如果栈上的变量的地址被存储到堆上,那么当栈帧销毁时,堆上的指针会变成“悬空指针”,导致程序崩溃。
-
指向栈对象的指针不能超过该栈对象的存活期
- 栈对象的生命周期很短,如果指针的生命周期比栈对象长,就会导致指针指向无效的内存。
类比:
- 栈对象就像一个临时工,工作完成后就会离开。
- 如果指针还指向这个临时工,但临时工已经走了,就会出问题。
逃逸分析的命令
go build -gcflags='-m -l' xxx.go
-m
:打印逃逸分析的优化策略。-l
:取消内联(避免内联优化干扰分析结果)。
类比:
- 这条命令就像是一个“侦探工具”,用来查看变量是否逃逸到堆上。
内存逃逸的影响
-
增加 GC 压力
- 堆上的数据需要垃圾回收器(GC)管理,逃逸到堆上的变量会增加 GC 的负担。
-
造成内存碎片
- 堆上的内存分配和释放不规则,容易导致内存碎片化,影响性能。
类比:
- GC 就像清洁工,堆上的变量越多,清洁工的工作量就越大。
- 内存碎片就像仓库里的空隙,浪费了空间。
什么时候会出现内存逃逸?
-
指针逃逸
- 当栈上的变量的地址被传递到堆上时,变量会被分配到堆上。
func foo() *int { x := 10 // x 是栈变量 return &x // 返回 x 的地址,导致 x 逃逸到堆 }
-
动态类型逃逸
- 使用接口(interface)等动态类型时,变量可能会被分配到堆上。
func bar() interface{} { x := 10 // x 是栈变量 return x // x 被包装成接口类型,可能逃逸到堆 }
-
栈空间不足
- 如果栈空间不足以存放变量,变量会被分配到堆上。
-
变量大小不确定
- 如果变量的大小在编译时无法确定,可能会被分配到堆上。
如何避免内存逃逸?
-
减少外部指针引用
- 避免将栈变量的地址传递到堆上。
func avoidEscape() { x := 10 // x 是栈变量 use(x) // 传递值而不是地址,避免逃逸 }
-
性能要求高的函数避免使用接口类型
- 接口类型会导致动态分配,尽量使用静态类型。
-
变量定义不要超过栈空间大小
- 栈空间有限(通常几 KB),大变量尽量分配到堆上。
-
使用逃逸分析工具优化代码
- 通过
-gcflags='-m -l'
查看哪些变量逃逸,并优化代码。
- 通过
总结
逃逸分析是 Go 语言优化性能的重要工具。虽然逃逸到堆上会增加 GC 压力,但在某些场景下是不可避免的。通过合理设计代码结构和使用工具,可以尽量减少不必要的逃逸,提高程序性能。