抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

什么是内存逃逸

在程序中, 每个函数块都会有自己的内存区域来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据, 这一块内存区域有特定的结构和寻址方式, 寻址起来十分迅速, 开销很少。这一块内存地址称为栈, 栈是线级别的, 大小在创建的时候已经确定, 当变量太大的时候, 会”逃逸”到堆上, 这种现象称为内存逃逸, 简单来说, 局部变量通过堆分配和回收, 就叫内存逃逸。

内存逃逸的危害

堆是一块没有特定结构, 也没有固定大小的内存区域, 可以根据需要进行调整。全局变量, 占用较大的局部变量, 函数调用结束后不能立刻回收的局部变量就会存在堆里面。变量在堆上的分配和回收都比在栈上开销大的多。对于go这种带GC的语言来说, 会增加GC压力, 同时容易造成内存碎片。

如何分析程序是否发生内存逃逸

build时添加-gcflags="-m"选项可分析内存逃逸情况。比如输出./main.go:3:6: moved to heap: x表示局部变量x逃逸到了堆上。

内存逃逸发生时机

1. 向channel发送指针数据

因为在编译时, 不知道channel中的数据会被哪个goroutine接收, 因此编译器没法知道变量什么时候会被释放, 因此只能放到堆中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

func main() {
ch := make(chan int, 1)
x := 5
ch <- 5 // x不发生逃逸, 因为只是复制值
ch1 := make(chan int, 1)
y := 5
py := &y
ch1 <- py // y逃逸, 因为y地址传入chan中, 编译时无法确定什么时候会被接收, 所以无法在函数返回后回收y
}


// 输出
# command-line-arguments
./test_Demo_04.go:5:6: can inline main
./test_Demo_04.go:16:2: moved to heap: y

2. 局部变量在函数调用结束后还被其他地方使用

局部变量在函数调用结束后还被其他地方使用, 比如函数返回局部变量指针或闭包中引用包外的值。因为变量的生命周期可能会超过函数周期, 因此只能放入堆中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

func foo() func() {
x := 5 // x发生逃逸, 因为在foo调用完成后, 被闭包函数用到, 还不能回收, 只能放到堆上存放
return func() {
x += 1
}
}

func main() {
inner := foo()
inner()
}

// 输出
# command-line-arguments
./test_Demo_04.go:5:6: can inline Foo
./test_Demo_04.go:7:9: can inline Foo.func1
./test_Demo_04.go:18:14: inlining call to Foo
./test_Demo_04.go:7:9: can inline main.Foo.func1
./test_Demo_04.go:19:7: inlining call to main.Foo.func1
./test_Demo_04.go:6:2: moved to heap: x
./test_Demo_04.go:7:9: func literal escapes to heap
./test_Demo_04.go:18:14: func literal does not escape

在slice或map中存储指针

比如[]*string, 其后面的数组可能是在栈上分配的, 但其引用的值还是堆上;

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main() {
var x int
x = 10
var ls []*int
ls = append(ls, &x) // x发生逃逸, ls存储的是指针, 所以ls底层的数组虽然在栈上存储, 但本身却逃逸到堆上
}

// 输出
# command-line-arguments
./test_Demo_04.go:5:6: can inline main
./test_Demo_04.go:11:6: moved to heap: x

4. 切片扩容

切片扩容后长度太长, 导致栈空间不足, 逃逸到堆上;

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main() {
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}

// 输出
# command-line-arguments
./test_Demo_04.go:5:6: can inline main
./test_Demo_04.go:11:11: make([]int, 10000) escapes to heap

5. 在interface类型上调用方法

在interface类型上调用方法时会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

type foo interface {
fooFunc()
}

type foo1 struct{}

func (f1 foo1) fooFunc() {}

func main() {
var f foo1
f = foo1{}
f.fooFunc() // 调用方法时, f发生逃逸, 因为方法是动态分配的
}

// 输出
# command-line-arguments
./test_Demo_04.go:12:6: can inline foo1.fooFunc
./test_Demo_04.go:16:6: can inline main
<autogenerated>:1: inlining call to foo1.fooFunc
./test_Demo_04.go:18:10: foo1{} escapes to heap

避免内存逃逸的方法

  • 对于小型数据, 使用传值而不是传指针, 避免内存逃逸;
  • 避免使用长度不固定的slice切片, 在编译期无法确定切片长度, 只能将切片使用堆分配;
  • interface调用方法会发生内存逃逸;

评论