Go内存逃逸

前言
很久以前就听过过内存逃逸这个词, 最近了解了一下, 才发现是个很简单的概念. 只要把前言部分看完, 就已经了解了. 来吧…

在介绍内存逃逸之前, 我们先用C语言来引出这个概念.

我们的进程在内存中有栈内存和堆内存的概念, 栈内存是函数执行的局部内存, 会随着函数的结束而全部释放, 而堆内存是需要手动申请和管理的一部分内存. 这个概念大家都比较熟悉了, 在此就不再赘述.

c语言版本
在C中, 如果我们在函数中想要返回一个整形数组, 怎么写呢? 比如这样?

#include "stdio.h"
int* test(){
    int a[2] = {1, 3};
    return a;
}
int main() {
    int* a= test();
    printf("address: %p, %d", a, a[1]);
}

如果你这样做了, 可能会发现读到的数组数据是正确的, 但在使用gcc编译的时候会报警, 提示返回的a变量是一个栈内存地址. 这是因为test执行结束后, 这部分内存未来就会被其他地方使用, 结果正确仅仅是因为内存中的内容还没有被修改.

那么正确的写法应该是什么呢? 比如这样:

#include "stdio.h"
#include "stdlib.h"
int* test(){
    int *a = (int*) malloc(2);
    a[0] = 1;
    a[1] = 2;
    return a;
}
int main() {
    int* a = test();
    printf("address: %p, %d", a, a[1]);
    free(a);
}

test函数中申请一段内存, 并将内存的指针返回. 申请的内存就保存在堆内存中. 但是, 这样一来, 就不能享受栈内存的自动释放了, 需要再使用后调用free释放内存, 以便后续使用.

Go版本

那么在Go中如果我们想在函数中返回一个数组, 怎么写呢?

package main
import "fmt"
func test() *[3]int {
    var a [3]int
    a = [3]int{1, 2, 3}
    return &a
}
func main() {
    a := test()
    fmt.Printf("address: %p, %d", a, a[1])
}

这段代码和上面C版本的功能相同, 都是返回了数组的地址. 那么问题来了, 为什么同样是局部变量, Go就可以在函数返回之后仍能读到呢?

原因很简单, Go的编译器在检测到数组指针会在函数外部使用时, 自行将其放到了堆内存中. 而这, 就是Go中所说的内存逃逸现象了. 是不是看过之后感觉只是一个很简单的道理换了个名词而已.

其实到这里, Go的内存逃逸已经介绍完了, 一句话介绍就是: 局部变量被放到了堆内存中.

逃逸情况
因为内存逃逸后会放到堆内存中, 需要依赖GC进行释放, 而栈内存会自动释放, 无需GC参与. 因此在开发中减少内存逃逸, 可以减轻GC压力.

既如此, 有没有办法在一个Go程序中检查哪里会发生内存逃逸呢? (逃逸是发生在编译期的呦). 就是build命令:

go build -gcflags '-m -l' main.go

-m: 打印逃逸分析内容. 最多的添加4个-m, 获取更详细的信息
all=-m: 若编译时不止一个文件, 对所有文件应用-m
-l: 禁用函数内联. 可以更准确的定位逃逸位置.
all=-l: 同理
好, 基于此, 我们简单介绍几种内存逃逸的情况, 更多的情况可自行摸索. (以下所有情况, 可自行通过build命令分析查看)

返回局部变量指针
比如前言中的情况, 再或者:

func test() *int {
    a := 5
    return &a
}

超出栈大小

若对象在栈中放不下了, 也会发生逃逸. 栈的大小可通过命令查看: ulimit -a | grep stack

func test() {
    // 当内存申请超出栈大小时, 逃逸
    _ = make([]int, 8192*1024/8)
    // 当使用变量进行初始化时, 因为无法预知变量的大小, 也会逃逸
  // 如果可以的话, 将 n 改为 const, 就可以避免内存逃逸
    n := 2
    _ = make([]int, n)
}

闭包

闭包也很好理解, 因为变量在函数返回之后仍需要访问, 因此需要逃逸到堆上.

func test() func() int {
    a := 0
    return func() int {
        return a
    }
}

fmt 包

当使用fmt包中的大部分函数时, 均会发生内存逃逸. 相关isuse8618 7218

func main() {
    // 没有发生内存逃逸
    _ = reflect.TypeOf("1")
    // string kind 等发发会发生内存逃逸
    _ = reflect.TypeOf("1").String()
    _ = reflect.TypeOf("1").Kind()
    // 会发生内存逃逸, 因为其内部调用了 reflect.TypeOf("223").String()
    // 调用链: Println->Fprintln->doPrintln->printArg->reflect.TypeOf(arg).String()
    fmt.Println("223")
}

具体原因未做分析, 感兴趣的可以查看其内部实现. 期待后续版本可以优化吧.

其他情况
切片扩容后栈空间不足
channel发送指针变量. stackoverflow
等等
总结
综上, 介绍了内存逃逸的概念及常见情况. 当发生逃逸的时候, 会增加GC的压力. 变量放在哪里简单来说就是:

若在函数外部使用了, 则必放在堆中
若在函数外部没有使用, 则优先放到栈中, 若栈中放不下, 则放到堆中
那么我们在函数返回结构体使经常碰到的疑问: 返回"值类型"还是"指针类型"??

如果返回"值类型"就不会发生逃逸, 但是会触发内存复制. 如果返回"指针类型"就无需内存复制, 但是会发生逃逸. 因此就需要在GC与内存复制之间进行平衡, 判断哪个开销比较大. 一般来说, 若变量占用内存较小, 传值更为合适. 若内存较大, 则传递指针更为合适. (不过, 一般的项目都没有到"需要考虑 GC"的情况吧???)

原文地址: https://hujingnb.com/archives/884

原文链接:https://blog.csdn.net/qq_31725391/article/details/128696183

posted @ 2023-06-07 16:14  ppjj  阅读(67)  评论(0编辑  收藏  举报