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
包中的大部分函数时, 均会发生内存逃逸. 相关isuse
: 8618 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