go语言内存逃逸是什么-亚博电竞手机版
go语言内存逃逸是什么
这篇文章主要介绍了go语言内存逃逸是什么的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇go语言内存逃逸是什么文章都会有所收获,下面我们一起来看看吧。
我们在高中学过一些天体物理的知识,比如常见的三个宇宙速度:
第一宇宙速度:航天器逃离地面围绕地球做圆周运动的最小速度:7.9km/s
第二宇宙速度:航天器逃离地球的最小速度:11.18km/s
第三宇宙速度:航天器逃离太阳系的最小速度:16.64km/s
了解了航天器的逃逸行为,我们今天来点特别的:内存逃逸。
通过本文你将了解到以下内容:
c/c 的内存布局和堆栈
go的内存逃逸和逃逸分析
内存逃逸的小结
part1c/c 的内存布局和堆栈
这应该是一道出现频率极高的面试题。
c/c 作为静态强类型语言,编译成二进制文件后,运行时整个程序的内存空间分为:
内核空间 kernel space
用户空间 user space
内核空间主要存放进程运行时的一些控制信息,用户空间则是存放程序本身的一些信息,我们来看下用户空间的布局:
堆和栈的主要特点:
栈区(stack):由编译器自动分配释放,存储函数的参数值,局部变量值等,但是空间一般较小数kb~数mb
堆区(heap):c/c 没有gc机制,堆内存一般由程序员申请和释放,空间较大,能否用好取决于使用者的水平
go语言与c语言渊源极深,c语言面临的问题,go同样会面对,比如:变量的内存分配问题。
在c语言中,需要程序员自己根据需要来确定采用堆还是栈,栈内存由os全权负责,但是堆内存需要显式调用malloc/new等函数申请,并且对应调用free/delete来释放。
go语言具有垃圾回收garbage collection机制来进行堆内存管理,并且没有像malloc/new这种堆内存分配的关键字。
栈内存的分配和释放开销非常小,堆内存对于go来说开销比栈内存大很多。
part2go的内存逃逸和逃逸分析
如果写过c/c 都会知道,在函数内部声明局部变量,然后返回其指针,如果外部调用则会报错:
#include
编译上述代码:main.cpp: in function ‘int* getvalue()’: main.cpp:7:9: warning: address of local variable ‘val’ returned [-wreturn-local-addr]
用同样的思想,写一个go版本的代码:
packagemainimport("fmt")funcmain(){str:=getstring()fmt.println(*str)}funcgetstring()*string{varsstrings="helloworld"return&s}
代码却可以正常运行,我们本意是在栈上分配一个变量,用完就销毁,但是外部却调用了,甚至可以正常进行,表现和c 完全不同。
其实,这就是go的内存逃逸现象,go模糊了栈内存和堆内存的界限,具体来说变量究竟分配到哪里,是由编译器来决定的。
1逃逸分析escape analysis
所谓逃逸分析就是在编译阶段由编译器根据变量的类型、外部使用情况等因素来判定是分配到堆还是栈,从而替代人工处理。
一般将局部变量和参数分配到栈上,但是并不绝对:
如果编译器不能确定在函数返回时,变量是否被使用则分配到堆上
如果局部变量非常大,也会分配到堆上
......
编译器不清楚局部变量是否会被外部使用时,就会倾向于分配到堆上。
go编译器在确定函数返回后不会再被引用时才分配到栈上,其他情况下都是分配到堆上。
这样做虽然浪费堆空间,但是有效避免了悬挂指针的出现,并且由于gc的存在也不会出现内存泄漏,权衡之下也是一种合理的做法。
2哪些情况会出现内存逃逸
对于go来说,在日常使用中有几种常见的做法会导致内存逃逸现象的出现:
指针逃逸
栈空间不足逃逸
map/slice/interface/channel的使用
......
指针逃逸
在上一个例子中我们使用一个int指针来说明内存逃逸的现象,接下来我们扩展一下变为结构体指针,并且使用gcflags来给编译器传特定参数来观察逃逸现象:
//test.gopackagemainimport"fmt"typeescapestruct{whostring}funccallinstance(callerstring)(*escape){instance:=new(escape)instance.who=callerreturninstance}funcmain(){outer:=callinstance("helloworld")fmt.println(outer.who)}
执行:go build -gcflags=-m test.go 如下:
#command-line-arguments./test.go:9:6:caninlinecallinstance./test.go:16:23:inliningcalltocallinstance./test.go:17:13:inliningcalltofmt.println./test.go:9:19:leakingparam:caller./test.go:10:17:new(escape)escapestoheap./test.go:16:23:mainnew(escape)doesnotescape./test.go:17:19:outer.whoescapestoheap./test.go:17:13:main[]interface{}literaldoesnotescape./test.go:17:13:io.writer(os.stdout)escapestoheap
我们可以看到"escapes to heap",确实出现了内存逃逸,本该在栈上逃逸到堆上了。
栈空间不足逃逸
对于64bit的linux系统而言栈的大小一般是8mb,go中每个goroutine初始化栈大小是2kb,在goroutine的运行过程中栈的大小可能会变化,但也不会超过os对线程栈大小的限制。
在网上找了个例子,用mac跑了一下:
packagemainimport"math/rand"funcgenerate8191(){nums:=make([]int,8191)//<64kbfori:=0;i<8191;i {nums[i]=rand.int()}}funcgenerate8192(){nums:=make([]int,8192)//=64kbfori:=0;i<8192;i {nums[i]=rand.int()}}funcgenerate(nint){nums:=make([]int,n)//不确定大小fori:=0;i
#command-line-arguments./test_3.go:6:14:generate8191make([]int,8191)doesnotescape./test_3.go:13:14:make([]int,8192)escapestoheap./test_3.go:20:14:make([]int,n)escapestoheap
可以看到在分配8191个大小时未发生逃逸,在分配8192时发生了逃逸,不定长度也发生了逃逸。
其他情况
在go中map、interface、slice、interface是非常常见的数据结构,也是非常容易触发内存逃逸的根源。
向channel中发送指针或者带指针的值,因为在编译时没有办法知道哪个goroutine会在channel上接收数据。所以编译器没法知道变量什么时候才会被释放。
slice中指针或带指针的值,这会导致切片的内容逃逸,尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
slice数组扩容也可能导致内存逃逸,如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
interface类型可以代表任意类型,编译器不知道参数会是什么类型,只有运行时才知道,因此只能分配到堆上。
part3内存逃逸小结
我们该如何评价内存逃逸呢?
go语言对用户来说模糊了堆内存和栈内存的分配,编译器借助于逃逸分析来实现特定场景的内存逃逸。
任何事情都是两面性,go语言借助于内存逃逸和gc机制解放了程序员,但是同时也带来了性能问题,因为堆内存的分配和释放都是需要成本的。
go的编译器在很多时候无法确定该如何分配内存,因此只能采用一种稳妥但有失性能的做法,分配到堆上。
意识里指针传递比值传递更高效,但是在go中并非如此,如果指针传递出现内存逃逸将内存分配到堆上后续就有会gc操作,消耗比值传递更大。
如果明确不需要外部使用,就需要尽量避免内存逃逸,不要一味完全依赖编译器本身。
关于“go语言内存逃逸是什么”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“go语言内存逃逸是什么”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注恰卡编程网行业资讯频道。