Goalng内存逃逸

本文深入探讨了Go语言中的逃逸分析,详细介绍了逃逸分析的原理及其在编译阶段的作用,通过具体代码示例展示了指针逃逸、栈空间不足逃逸、动态分配逃逸、闭包引用对象逃逸及内存跨栈使用逃逸等场景,帮助开发者理解如何有效利用堆内存,减轻GC压力。

无GC语言是怎么运作的

一般来说,类似C/C++语言通过 malloc等方法分配的内存是在heap上的,但在Golang中却不是这样的,即便使用 new,也不一定分配在heap上,这也是我们今天要关注的问题。

那么为什么Go会这样呢?

其实也很简单,因为Go是有runtime的,实际分配在heap还是stack是由runtime决定的。看到这里可能会很奇怪,为什么有runtime,有GC我们还要关心分配在哪里呢?一切交给runtime和GC去管理就好了呀!

是的,确实是这样,但是有效的使用堆内存,避免因系统GC造成的STW带来的性能损失对于某些系统来说也是比较重要的。因此,接下来将说明Go的runtime如何决定变量分配在哪里的。

什么是逃逸分析?

在进入下面的内容前,先来解释下什么是逃逸分析。

其实很简单

逃逸分析就是由编译器确定内存在heap还是在stack,而不是程序员决定。

如果在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束可自动将内存回收;
  • 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;

那怎么确定发生了逃逸呢?

如果本该分配在stack上的内存分配到了heap,则发生了逃逸。

逃逸的场景在Go中有多个,下面将详细介绍

逃逸场景

要有效的使用堆内存,就需要了解什么情况下发生逃逸。

场景一:指针逃逸

因为Go有指针机制,因此我们可以在函数结束时返回一个指针

package main

func main() {
    _ = f(10, 20)
}

func f(x, y int) *int {
    n := new(int)
    *n = x * y
    return n
}

这段代码发生了逃逸,我们用 go build gcflags "-m -l" 来查看,其中 -l是阻止内联优化

.\main.go:8:10: new(int) escapes to heap

编译时显示第8行代码分配在heap上,也就是 n := new(int) 这段代码

场景二:栈空间不足逃逸

当我们分配的变量内存超过stack空间时,也会发生逃逸,如下代码

package main

func main() {
    _ = make([]int, 1000, 1000)
}

编译时提示

.\main.go:4:10: main make([]int, 1000, 1000) does not escape

没有发生逃逸,也就意味着没有超过stack空间,我们加大内存分配

package main

func main() {
    _ = make([]int, 1000, 10000)
}

将数量调整至 10000,再进行编译提示

.\main.go:4:10: make([]int, 1000, 10000) escapes to heap

发生了逃逸

场景三:动态分配逃逸

比如下面这段代码

package main

func main() {
    l := 10
    _ = make([]int, l)
}

编译时提示

.\main.go:5:10: make([]int, l) escapes to heap

发生逃逸的原因很简单,因为 l变量可能被更改,所以编译器认为应该分配到 heap

场景四:闭包引用对象逃逸

Go语言支持闭包机制的,因此也会发生逃逸,如下代码

package main

func main() {
    _ = f()
}

func f() func() int {
    a, b := 0, 1
    return func() int {
        return a + b
    }
}

编译时提示

.\main.go:9:9: func literal escapes to heap
.\main.go:9:9: func literal escapes to heap

本来ab作为函数局部变量应该分配到stack中,但是由于f()函数返回了一个闭包函数,因此编译器认为ab应该分配到heap

场景五:其他函数栈使用了该内存

package main

import "fmt"

func main() {
    a := new(int)
    fmt.Println(a)
}

编译时提示

.\main.go:7:13: a escapes to heap
.\main.go:6:10: new(int) escapes to heap
.\main.go:7:13: main ... argument does not escape

总结

逃逸分析在编译阶段完成,逃逸分析目的是决定内存地址分配在stack上还是heap上。

通过了解逃逸分析,我们在编写Go代码时就可以知道内存究竟是在heap上分配还是在stack上分配,然后更精细的控制heap上的内存分配,减轻GC压力。

### Go语言内存逃逸原理与分析 Go语言中的内存逃逸现象是指某些变量在编译时被分配到堆上,而不是栈上的情况。这种现象的发生取决于编译器的逃逸分析结果[^1]。逃逸分析的主要目的是通过静态分析代码来决定变量是否需要从栈中“逃逸”到堆中进行分配。 #### 1. 内存逃逸的原因 当一个对象的生命周期超出了当前函数的作用域时,编译器会将该对象分配到堆上以确保其在函数返回后仍然可以被访问。例如,如果一个局部变量作为返回值传递给外部调用者,或者被存储到全局变量、闭包或其他长生命周期的数据结构中,那么这个变量就会发生内存逃逸[^4]。 #### 2. 逃逸分析的过程 逃逸分析的算法主要包括以下几个步骤: - **构建抽象语法树(AST)**:对源代码进行解析,生成抽象语法树。 - **数据流分析**:确定变量的作用域和生命周期,检查变量的引用是否会逃逸到函数外部[^2]。 - **分配决策**:根据分析结果,决定变量是分配在栈上还是堆上。 以下是一个简单的例子,展示了逃逸分析的结果: ```go package main import "fmt" func foo() *int { x := 10 // x 是一个局部变量 return &x // 返回 x 的地址,导致 x 被分配到堆上 } func main() { y := foo() // y 持有堆上分配的变量的指针 fmt.Println(*y) } ``` 在上述代码中,`x` 是一个局部变量,但由于它的地址被返回并赋值给了 `y`,因此 `x` 必须分配到堆上以保证其在函数返回后仍然有效[^3]。 #### 3. 内存逃逸的影响 - **性能影响**:堆上的内存分配和垃圾回收(GC)通常比栈上的内存管理更加昂贵,因此频繁的内存逃逸可能会降低程序的运行效率[^4]。 - **内存使用优化**:通过合理设计代码,减少不必要的内存逃逸,可以提高程序的性能和内存利用率。 #### 4. 常见的内存逃逸场景 - **返回局部变量的指针或地址**:如上例所示,当函数返回局部变量的地址时,该变量会被分配到堆上。 - **闭包捕获变量**:如果闭包捕获了某个局部变量,并且该闭包的生命周期超出当前函数的作用域,则被捕获的变量会被分配到堆上。 - **切片(slice)和映射(map)作为参数或返回值**:当切片或映射作为函数的返回值时,它们的底层数据结构通常会被分配到堆上[^5]。 #### 示例代码 以下代码展示了切片作为返回值时发生的内存逃逸: ```go package main import "fmt" func createSlice() []int { s := make([]int, 5) // 创建一个长度为5的切片 for i := 0; i < 5; i++ { s[i] = i } return s // 切片的底层数组被分配到堆上 } func main() { slice := createSlice() fmt.Println(slice) } ``` 在上述代码中,`s` 是一个局部变量,但由于它被作为返回值返回,因此其底层数组会被分配到堆上。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

anssummer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值