您的位置 首页 golang

Golang面试必问——内存逃逸分析

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

1,什么是内存逃逸。

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

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

4,怎么避免内存逃逸。


什么是内存逃逸

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

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

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


内存逃逸的场景有哪些

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

 go build -gcflags "-m" main.go  

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


局部指针返回

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

 package main

 import  (
  "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 main

import (
  "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 main

import (
  "fmt"
)

func main() {
  fmt.Println("关注公众号:不穿格子衫的程序猿")
}  

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


闭包调用

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

 package main

import (
  "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份。

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

如果大家有比较好的资源欢迎相互交流,共同提高。同时有部分素材来源于网络,如侵,联删

文章来源:智云一二三科技

文章标题:Golang面试必问——内存逃逸分析

文章地址:https://www.zhihuclub.com/88604.shtml

关于作者: 智云科技

热门文章

网站地图