Administrator
发布于 2024-07-12 / 2 阅读
0

内存逃逸分析

内存逃逸分析是 Go 语言中的一个重要概念。

在了解内存逃逸之前,需要先知道栈内存和堆内存的概念:

  • 堆内存(Heap):一般由人为手动进行管理,手动申请、分配、释放。其大小一般取决于硬件内存,适合不可预知大小的内存分配,但分配速度较慢,且可能会形成内存碎片。

  • 栈内存(Stack):是一种具有特殊规则的线性表数据结构,由编译器自动进行管理,自动申请、分配、释放。其大小通常是固定的。

Go 编译器会通过逃逸分析来决定变量分配在栈上还是堆上。逃逸分析是用于堆和栈分配的选择,编译器会追踪变量在代码块的作用域,判断变量在整个运行周期是否完全可知,若可以则在栈上分配;否则逃逸到堆上。

Go 语言工具链提供了查看对象是否逃逸的方法。在执行go build时,可以配合使用参数-gcflags开启编译器支持的额外功能。例如,使用go build -gcflags '-m -l' main.go,其中-m会打印出逃逸分析的优化策略,-l会禁用函数内联,以更好地观察逃逸情况,减少干扰。

此外,还可以通过反编译命令go tool compile -S main.go来更底层、更准确地判断一个对象是否逃逸。

一些常见的导致内存逃逸的情景包括:

  • 在方法内把局部变量指针返回:局部变量原本应在栈中分配和回收,但由于返回时被外部引用,其生命周期大于栈,从而发生逃逸。

func getPointer() *int {
    num := 10
    return &num
}

在上述代码中,函数getPointer返回了局部变量num的指针,由于函数返回后局部变量num的生命周期并未结束,所以num会逃逸到堆上

  • 在切片上存储指针或带指针的值:例如(*string),这会导致切片的内容逃逸,尽管后面的数组可能在栈上分配,但其引用的值一定在堆上。如果切片的背后数组因append操作可能超出其容量而被重新分配,也会在堆上分配。

func expandSlice() {
    s := make([]int, 0, 1)
    for i := 0; i < 10; i++ {
        s = append(s, i)
    }
}

当切片不断扩容,可能会导致底层数组重新分配内存,从而使元素发生逃逸。

func closureExample() {
    num := 10
    f := func() {
        fmt.Println(num)
    }
    f()
}

闭包中引用的外部变量可能会逃逸到堆上。

  • interface类型上调用方法:由于方法的真正实现只能在运行时知道,这是动态调度,所以也可能发生逃逸。

func interfaceExample() {
    num := 10
    var i interface{} = num
}

当变量被赋值给 interface{} 类型时,可能会发生逃逸。

  • 大尺寸的结构体或数组
    如果在函数内部创建了较大尺寸的结构体或数组,且其使用超出了函数的作用域,也可能发生逃逸。逃逸分

析内存逃逸分内存逃逸分析析