前言
很久以前就听过过内存逃逸这个词, 最近了解了一下, 才发现是个很简单的概念. 只要把前言部分看完, 就已经了解了. 来吧...
在介绍内存逃逸之前, 我们先用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
发送指针变量- 等等
总结
综上, 介绍了内存逃逸的概念及常见情况. 当发生逃逸的时候, 会增加GC
的压力. 变量放在哪里简单来说就是:
- 若在函数外部使用了, 则必放在堆中
- 若在函数外部没有使用, 则优先放到栈中, 若栈中放不下, 则放到堆中
那么我们在函数返回结构体使经常碰到的疑问: 返回"值类型"还是"指针类型"??
如果返回"值类型"就不会发生逃逸, 但是会触发内存复制. 如果返回"指针类型"就无需内存复制, 但是会发生逃逸. 因此就需要在GC
与内存复制之间进行平衡, 判断哪个开销比较大. 一般来说, 若变量占用内存较小, 传值更为合适. 若内存较大, 则传递指针更为合适. (不过, 一般的项目都没有到"需要考虑 GC"的情况吧???)
原文地址: https://hujingnb.com/archives/884
标签:int,test,逃逸,内存,func,Go From: https://www.cnblogs.com/hujingnb/p/17053820.html