内存逃逸常见情况和避免方式

什么是内存逃逸

局部变量本来分配在栈上,但是可能因为变量太大等情况导致分配到对上的情况称为内存逃逸。

方式

编译器可以证明变量在函数返回后不再被引用,才会分配到栈上,否则分配到堆上。

栈上分配(静态内存分配),一般由系统进行申请和释放,例如:(函数的入参、局部变量、返回值等),每个函数都会分配一个栈帧,在函数运行结束后进行销毁

堆上分配(动态分配内存),在函数运行结束后仍然可以使用,如果要回收掉,需要进行GC,带来额外的性能开销。

逃逸机制

编译器会根据变量是否被外部引用来决定是否逃逸:

  • 函数没有外部引用,优先放在栈中;
  • 函数外部存在引用,放在堆中;
  • 栈上放不下,必定放在堆中;

逃逸分析由编译器决定那些变量放在栈中,哪些放在堆中,通过编译参数 -gcflags=-m,可以查看编译过程中的逃逸分析。

场景

指针逃逸

函数返回值为局部变量的指针,函数虽然退出了,但因为指针的存在,指向的内存不能随着函数的结束而回收,因此只能分配在堆上

package main
func escape1() * int {
    var a int = 1
    return &a
}
func main() {
    escape1()
}

image

栈空间不足

package main

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

image

变量大小不确定

package main

func escape3() {
    number := 10
    s := make([]int, number)
    for i:=0; i<len(s); i++ {
        s[i] = i
    }
}

func main() {
    escape3()
}

image

编译期间无法确定slice的长度,这种情况为了保证内存的安全,编译期也会触发逃逸,在堆上进行内存分配。

直接 s:= make([]int, 10),不会发生逃逸

动态类型

动态类型就是编译期间不确定参数的类型,参数的长度也不确定的情况下就会发生逃逸,空接口interface{}可以表示任意的类型,如果函数参数为interface{},编译期间就很难确定其参数的具体类型,也会发生逃逸。

package main

import "fmt"

func escape4() {
    fmt.Println(111111)
}

func main() {
    escape4()
}

image

闭包引用对象

闭包函数中,局部变量在后续函数是继续使用的,编译器将其分配到堆上。

package main

func escape5() func() int {
    var i int = 1
    return func() int {
        i++
        retunr i
    }
}
func main() {
    escape5()
}

image

总结

  1. 栈上分配内存比堆上分配内存效率更高;
  2. 栈上分配的内存不需要GC处理,而堆的需要;
  3. 逃逸分析目的是决定内存分配地址是栈还是堆;
  4. 逃逸分析在编译阶段完成;
  5. 无论变量大小,只要是指针变量都会在堆上分配,所以对于小变量使用传值(而不是传指针),效率更高。
posted @ 2025-08-09 16:37  biby  阅读(16)  评论(0)    收藏  举报