这个题是小编面试遇到次数最多的题目之一了。在开始之前,我们先思考以下几个问题,当然,后面小编也会一一解答。
1,什么是内存逃逸。
2,内存逃逸的场景有哪些。
3,分析内存逃逸的意义。
4,怎么避免内存逃逸。
什么是内存逃逸在了解什么是内存逃逸之前,我们先来简单地熟悉一下两个概念。栈内存和堆内存。本次主要是讲述的是Golang的内存逃逸,故而关于内存分配和垃圾回收就不做赘述了。后面小编会单独出两篇来写这个,有需要的同学可以关注小编。关于这一块,我们现在只需要了解三点。
- Golang的GC主要是针对堆的,不是栈。
- 引用类型的全局变量分配在堆上,值类型的全局变量分配在栈上。
- 局部变量内存分配可能在栈上也可能在堆上。
有了前面的基础知识,那我们简单粗暴地介绍一下内存逃逸。一个对象本应该分配在栈上面,结果分配在了堆上面,这就是内存逃逸。如下
内存逃逸的场景有哪些要了解内存逃逸的场景,首先我们要学会怎么分析内存逃逸。其实分析起来很简单,只需要一条简单的命令,即gcflags。这个是有很多参数的,此处只举一个最基本的例子。
go build -gcflags '-m' main.go
接下来我们就来讨论一下内存逃逸的场景有哪些。常见的场景有四种,小编总结为:局部指针返回,栈空间不足,动态类型,闭包引用。
局部指针返回
当我们在某个方法内定义了一个局部指针,并且将这个指针作为返回值返回时,此时就发生了逃逸。这种类型的逃逸是比较常见的,如下。
package mainimport ( 'fmt')func main() { str := returnPointer() fmt.Println(*str)}// 返回局部指针func returnPointer() *string { str := '更多免费资料,关注公众号:不穿格子衫的程序猿' return &str}
栈空间不足
众所周知,在系统中栈空间相比与总的内存来说是非常小的。如下,小编的Mac是16G*512G的,可是整个系统中栈空间大小也才8M。
而在我们的实际编码过程中,大部分Goroutine的占用空间不到10KB(这也是Golang能支持高并发的原因之一)。而其中分配给栈的更是少之又少。所以一旦某个对象体积过大时候就会发生逃逸,从栈上面转到堆上面。
如下,有两个map,space1和space2,space1长度大小都是100,space2长度大小都是10000,结果space2发生了逃逸,space1没有。
package mainimport ( 'fmt')func main() { space() fmt.Println('更多免费资料,关注公众号:不穿格子衫的程序猿')}// 栈空间溢出func space() { // 不溢出 space1 := make([]int, 100, 100) for i := 0; i < len(space1); i++ { space1[i] = i } // 溢出 space2 := make([]int, 10000, 10000) for i := 0; i < len(space2); i++ { space2[i] = i }}
动态类型
小编认为,这种内存逃逸应该是最多的,最常见的,而且还无法避免。简单地说就是被调用函数的入参是interface或者是不定参数,此时就会发生内存逃逸。如下:
package mainimport ( 'fmt')func main() { fmt.Println('关注公众号:不穿格子衫的程序猿')}
哈哈哈,同学们是不是大跌眼镜,一个简简单单的Println居然也会发生内存逃逸。那么问题来了,这个是怎么导致的呢,废话不多说,直接拔掉底裤撸源码。此处就是所谓的动态类型。
闭包调用
首先说一下,这种场景是非常少的,一般没有人写这种可读性这么差的代码,小编这串代码都是参考别人的。所以小编认为,这种场景,我们只需要知道即可,大概率是碰不上的。
package mainimport ( 'fmt')func main() { fmt.Println(closure())}// 闭包逃逸func closure() func() string { return func() string { return '更多免费资料,关注公众号:不穿格子衫的程序猿' }}
分析内存逃逸的意义前面给大家列举了四种内存逃逸的场景,那么问题来了,分析内存逃逸有什么用呢?简单的总结就是两点:减轻GC的压力,提高分配速度。
上文已经说过,Golang的GC主要是针对堆的,而不是栈。试想一下,如果大量的对象从栈逃逸到堆上,是不是就会增加GC的压力。在GC的过程中会占用比较大的系统开销(一般可达到CPU容量的25%)。而且目前所有的GC都有STW这个死结,而STW会造成用户直观的'卡顿'。非常影响用户体验。
此外,堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆内存分配首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。
通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少GC的压力,提高程序的运行速度。
怎么避免内存逃逸最后说一下怎么避免内存逃逸吧。首先需要注意的是,Golang在编译的时候就可以确立逃逸,并不需要等到运行时。这样就给了咱们避免内存逃逸的机会。
首先咱们明确一点,小编认为没有任何方式能绝对避免内存逃逸。原因嘛,就是存在【动态类型】这种逃逸方式,几乎所有的库函数都是动态类型的。当然也不是说咱么要破罐子破摔,该避免还是要避免一下的,主要的原则有以下几种,分别针对上面几种场景。
- 尽量减少外部指针引用,必要的时候可以使用值传递。
- 对于自己定义的数据大小,有一个基本的预判,尽量不要出现栈空间溢出的情况。
- Golang中的接口类型的方法调用是动态调度,如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型。
- 尽量不要写闭包函数,可读性差还逃逸。
又到了大家期待的福利时间了。本次赠送的是Golang实战案例20份。
废话不多说,各位看官大人要怎么获取呢。很简单,关注小编,私信「资料」即可获得免费获取方式。