这个题是小编面试遇到次数最多的题目之一了。在开始之前,我们先思考以下几个问题,当然,后面小编也会一一解答。

1,什么是内存逃逸。

2,内存逃逸的场景有哪些。

3,分析内存逃逸的意义。

4,怎么避免内存逃逸。

什么是内存逃逸

在了解什么是内存逃逸之前,我们先来简单地熟悉一下两个概念。栈内存堆内存。本次主要是讲述的是Golang的内存逃逸,故而关于内存分配和垃圾回收就不做赘述了。后面小编会单独出两篇来写这个,有需要的同学可以关注小编。关于这一块,我们现在只需要了解三点。

  1. Golang的GC主要是针对堆的,不是栈。
  2. 引用类型的全局变量分配在堆上,值类型的全局变量分配在栈上。
  3. 局部变量内存分配可能在栈上也可能在堆上。

有了前面的基础知识,那我们简单粗暴地介绍一下内存逃逸。一个对象本应该分配在栈上面,结果分配在了堆上面,这就是内存逃逸。如下

文章图片1
内存逃逸的场景有哪些

要了解内存逃逸的场景,首先我们要学会怎么分析内存逃逸。其实分析起来很简单,只需要一条简单的命令,即gcflags。这个是有很多参数的,此处只举一个最基本的例子。

go build -gcflags '-m' main.go

接下来我们就来讨论一下内存逃逸的场景有哪些。常见的场景有四种,小编总结为:局部指针返回,栈空间不足,动态类型,闭包引用

局部指针返回

当我们在某个方法内定义了一个局部指针,并且将这个指针作为返回值返回时,此时就发生了逃逸。这种类型的逃逸是比较常见的,如下。

文章图片2
package mainimport (  'fmt')func main() {  str := returnPointer()  fmt.Println(*str)}// 返回局部指针func returnPointer() *string {  str := '更多免费资料,关注公众号:不穿格子衫的程序猿'  return &str}

栈空间不足

众所周知,在系统中栈空间相比与总的内存来说是非常小的。如下,小编的Mac是16G*512G的,可是整个系统中栈空间大小也才8M。

文章图片3

而在我们的实际编码过程中,大部分Goroutine的占用空间不到10KB(这也是Golang能支持高并发的原因之一)。而其中分配给栈的更是少之又少。所以一旦某个对象体积过大时候就会发生逃逸,从栈上面转到堆上面。

如下,有两个map,space1和space2,space1长度大小都是100,space2长度大小都是10000,结果space2发生了逃逸,space1没有。

文章图片4
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或者是不定参数,此时就会发生内存逃逸。如下:

文章图片5
package mainimport (  'fmt')func main() {  fmt.Println('关注公众号:不穿格子衫的程序猿')}

哈哈哈,同学们是不是大跌眼镜,一个简简单单的Println居然也会发生内存逃逸。那么问题来了,这个是怎么导致的呢,废话不多说,直接拔掉底裤撸源码。此处就是所谓的动态类型。

文章图片6

闭包调用

首先说一下,这种场景是非常少的,一般没有人写这种可读性这么差的代码,小编这串代码都是参考别人的。所以小编认为,这种场景,我们只需要知道即可,大概率是碰不上的。

文章图片7
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在编译的时候就可以确立逃逸,并不需要等到运行时。这样就给了咱们避免内存逃逸的机会。

首先咱们明确一点,小编认为没有任何方式能绝对避免内存逃逸。原因嘛,就是存在【动态类型】这种逃逸方式,几乎所有的库函数都是动态类型的。当然也不是说咱么要破罐子破摔,该避免还是要避免一下的,主要的原则有以下几种,分别针对上面几种场景。

  1. 尽量减少外部指针引用,必要的时候可以使用值传递。
  2. 对于自己定义的数据大小,有一个基本的预判,尽量不要出现栈空间溢出的情况。
  3. Golang中的接口类型的方法调用是动态调度,如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型。
  4. 尽量不要写闭包函数,可读性差还逃逸。
资料环节

又到了大家期待的福利时间了。本次赠送的是Golang实战案例20份。

文章图片8

废话不多说,各位看官大人要怎么获取呢。很简单,关注小编,私信「资料」即可获得免费获取方式。