Golang内存逃逸
文章目录
- 1、什么是逃逸
- 2、逃逸问题相关
- 3、导致内存逃逸的原因是什么
- 4、常见发生逃逸的情况和分析
- 5、逃逸如何分析
1、什么是逃逸
逃逸是指在函数内部创建的对象或变量,在函数结束后仍然被其他部分引用或持有(逃离了变量的作用域)
2、逃逸问题相关
- 对内存管理的理解栈
- 栈上的内存分配和释放由编译器自动管理,快但是空间有限
- 堆:堆上的内存分配需要运行时系统的参与,相对慢但空间大,由GC回收
- 逃逸可能带来的影响:
- 分配和回收开销增加
- 指针引用和内存安全
- 内存泄露风险
- 识别逃逸场景,规避不必要的内存逃逸
- 【并不是所有的逃逸都是不被允许的(闭包函数)】
3、导致内存逃逸的原因是什么
- 栈空间是否足够
- 大小是否足够:如果有大变量,肯定会逃逸到堆上
- 变量作用域
- 变量或者对象没有以指针的方式返回地址,整个生命周期都在函数内部,随着函数销毁而销毁的话,肯定就不会有逃逸
- 编译时无法确定类型或大小
- interface的多态实现,不给他赋值,会在堆上分配
- 定义切片,不确定大小,也有可能导致内存逃逸
- Golang的两大内存分配基本原则
- 栈上的对象只存在栈上,但不能把指针存储到对堆中
- 栈上指针的生命周期不能超过对象的生命周期
4、常见发生逃逸的情况和分析
-
指针、slice和map作为返回值
- 当带有指针的返回值被赋给外部变量或者作为参数传递给其他函数时,编译器无法确定该变量何时停止使用,所以这些变量的内存都应该分配到堆中,逃离当前函数的作用域「逃逸到堆中」
func f (*int ,[]int, map[int]int) {... }
-
向Chan中发送数据的指针或者包括指针的值
- 可能会被其他协程或者方法引用
func f() {ch :=make(chan *int, 2)ch <- &i<-ch }
-
非直接的函数调用
- 闭包中引用包外的值,因为闭包执行的生命周期可能会超过函数周期,因此需要放入堆中
func f() func() {i := 1return func() {fmt.Println(i)} }
-
在slice或map中存储指针或者包含指针的值
- 当把一个指针或者包含指针的值放入slice或map中,编译器无法确定该指针所引用的数据是否会在函数返回后仍然被使用,为了保证数据有效性,会把它分配在堆上。
func f() {i := 1list := make([]*int, 10)list[0] = &i }
-
interface类型多态的应用,可能会导致逃逸
- 接口类型可以持有任意实现了该接口的类型,编译器在编译的时候无法确定具体的动态类型。所以编译器会将接口对象分配到堆上
func f() {var a action = run{}a.fast()var a1 actiona1 = run{}a1.fast() }type action interface {fast() }type run struct{}func (r run) fast() {}
5、逃逸如何分析
go build -gcflags -m .\main.gomove to heap: i[]int{...} escapes to heapfunc literal escapes to heaprun{} escapes to heap
- 实际情况例子
main\main.go:28:6: can inline reverseBetween
main\main.go:19:16: inlining call to reverseBetween
main\main.go:5:7: &ListNode{...} escapes to heap
main\main.go:7:9: &ListNode{...} escapes to heap
main\main.go:9:10: &ListNode{...} escapes to heap
main\main.go:11:11: &ListNode{...} escapes to heap
main\main.go:19:16: &ListNode{...} does not escape
main\main.go:28:21: leaking param: head
main\main.go:30:15: &ListNode{...} does not escape
在终端中执行命令分析
!避免不必要的逃逸!