Go的内存逃逸
内存逃逸是 Go 语言中一个重要的概念,指的是本应分配在栈上的变量被分配到了堆上。栈上的变量在函数结束后会自动回收,而堆上的变量需要通过垃圾回收(GC)来管理,因此内存逃逸会增加 GC 的压力,影响程序性能。
1. 什么是内存逃逸?
内存逃逸是指本应分配在栈上的变量,由于某些原因被分配到了堆上。栈上的变量在函数结束后会自动回收,而堆上的变量需要通过垃圾回收(GC)来管理。
栈和堆的区别
- 栈:
- 分配和回收速度快。
- 变量生命周期与函数绑定,函数结束后自动回收。
- 适合存储局部变量和小型数据。
- 堆:
- 分配和回收速度较慢。
- 变量生命周期不固定,需要 GC 管理。
- 适合存储大型数据或需要跨函数共享的数据。
2. 内存逃逸的影响
内存逃逸会增加 GC 的压力,影响程序性能:
- 性能开销:堆上的变量需要通过 GC 回收,增加了额外的性能开销。
- 延迟增加:GC 的执行可能会导致程序暂停(STW),增加延迟。
3. 内存逃逸的发生条件
以下是一些常见的内存逃逸场景:
3.1 方法内返回局部变量指针
- 原因:局部变量的指针被返回,导致其生命周期超出函数范围。
package main
import "fmt"
func foo() *int {
x := 42
return &x // x 逃逸到堆上
}
func main() {
p := foo()
fmt.Println(*p) // 输出: 42
}
3.2 向 channel 发送指针数据
- 原因:指针数据被发送到 channel,可能导致其生命周期超出当前函数。
package main
import "fmt"
func foo(ch chan *int) {
x := 42
ch <- &x // x 逃逸到堆上
}
func main() {
ch := make(chan *int, 1)
foo(ch)
p := <-ch
fmt.Println(*p) // 输出: 42
}
3.3 在闭包中引用包外的值
- 原因:闭包中引用的外部变量需要延长其生命周期。
package main
import "fmt"
func foo() func() int {
x := 42
return func() int {
return x // x 逃逸到堆上
}
}
func main() {
f := foo()
fmt.Println(f()) // 输出: 42
}
3.4 在 slice 或 map 中存储指针
- 原因:slice 或 map 中存储的指针可能导致其生命周期超出当前函数。
package main
import "fmt"
func foo() []*int {
x := 42
return []*int{&x} // x 逃逸到堆上
}
func main() {
s := foo()
fmt.Println(*s[0]) // 输出: 42
}
3.5 切片(扩容后)长度太大
- 原因:切片扩容后,数据可能被重新分配到堆上。
package main
import "fmt"
func foo() {
s := make([]int, 0, 10)
for i := 0; i < 10000; i++ {
s = append(s, i) // s 可能逃逸到堆上
}
fmt.Println(len(s)) // 输出: 10000
}
func main() {
foo()
}
3.6 在 interface 类型上调用方法
- 原因:interface 类型的动态分发可能导致变量逃逸。
package main
import "fmt"
type MyInterface interface {
DoSomething()
}
type MyStruct struct {
x int
}
func (m *MyStruct) DoSomething() {
fmt.Println(m.x)
}
func foo() MyInterface {
x := 42
return &MyStruct{x} // x 逃逸到堆上
}
func main() {
obj := foo()
obj.DoSomething() // 输出: 42
}
Mermaid 流程图
以下是内存逃逸的流程图: