跳过正文
  1. 面试题库/

11|内存相关

·2262 字·5 分钟
目录
Golang面试题库 - 这篇文章属于一个选集。
§ 11: 本文

1. 谈谈内存泄露,什么情况下内存会泄露?怎么定位排查内存泄漏问题?
#

分析

首先要明白什么是内存泄漏。从字面意思很好理解,就是程序中存在内存不能及时被有效释放,导致这部分内存不可用。随着越来越多的可能出现内存泄漏,会出现内存用满,程序崩溃的情况。

理解了内存泄漏之后,就可以思考一下 Go 语言中编码哪些情况会出现这种情况。其实最常见的就是 goroutine 的阻塞不能快速释放,导致这部分内存一直占用着。随着 goroutine 越来越多,就内存泄漏了。

什么是内存泄漏
#

内存泄漏就是程序生命周期中一些对象不能被及时回收,一直占用着内存,导致这部分内存不可用的情况。

Go 语言内存泄露原因
#

分析

Go 语言中内存泄漏其实场景很多,像 time.NewTicker 资源未及时释放、select 阻塞等等。其实大部分情况都是由于阻塞或者程序空转,导致程序不能结束,一直占用内存。

回答

Go 语言的内存泄漏一般是由于程序阻塞或者空转,导致程序不能及时结束导致的。常见的情况就是 goroutine 的阻塞,或者空转,比如 goroutine 没有被关闭,或者没有添加超时控制,让 goroutine 一直处于阻塞状态,不能被 GC。

还有一种就是一些资源句柄没有释放,比如文件打开未关闭等等。然后就是 goroutine 内部的业务逻辑不合理,比如:

  • Goroutine 内进行 channel / mutex 等读写操作被一直阻塞。
  • Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
  • Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。

常见的 goroutine 泄漏场景
#

  • channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。
  • channel 发送数量超过 channel 接收数量,就会造成阻塞。
  • channel 接收数量超过 channel 发送数量,也会造成阻塞。
  • http request body 未关闭,goroutine 不会退出。
  • 互斥锁忘记解锁。
  • sync.WaitGroup 使用不当。
  • time.Ticker 是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用 Stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间。

如何排查
#

分析

对于 Go 语言的性能分析,比如 CPU、内存一般会使用 pprof 工具来进行分析。

回答

  • 单个函数:调用 runtime.NumGoroutine 方法来打印执行代码前后 Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。
  • 生产/测试环境:使用 PProf 实时监测 Goroutine 的数量。

2. 知道 Golang 的内存逃逸吗?什么情况下会发生内存逃逸?
#

什么是内存逃逸
#

分析

要知道内存逃逸,其实首先得明白 Go 语言的逃逸分析。Go 语言的逃逸分析是在编译期决定对象分配方式的一种策略,决定对象时分配在堆上还是分配在栈上。

就是优先本来可以分配到栈上的对象被分配到堆上了,这个时候就发生了逃逸。

具体的逃逸分析策略有以下几种:

  1. 如果函数外部没有引用,则优先放到栈中。
  2. 如果函数外部存在引用,则必定放到堆中。
  3. 如果栈上放不下,则必定放到堆上。

前两种主要考虑对象的生命周期,当这个对象的生命周期不能被确定,不能跟随当前函数结束而结束,就会发生逃逸,被分配到堆上。

回答

内存逃逸是编译器在程序编译时期根据逃逸分析策略,将原本应该分配到栈上的对象分配到堆上的一个过程。

什么情况下会发生内存逃逸
#

以下场景会发生内存逃逸:

  • 方法内返回局部变量指针。
  • channel 发送指针数据。
  • 在闭包中引用包外的值。
  • slicemap 中存储指针。
  • 切片(扩容后)长度太大。
  • interface 类型上调用方法。

内存逃逸有什么影响
#

分析

这个问题可以从栈对象和堆对象的区别来回答。堆对象需要垃圾回收机制来释放内存,栈对象会跟随函数结束被编译器回收。

回答

大量的内存逃逸会给 GC 带来压力。

3. 请简述 Go 是如何分配内存的?
#

分析

Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheapmspanarenasmcentralmcache。它们以 mspan 作为基本分配单位。

具体的分级策略回顾内存模型图:

alt text

回答

Go 语言对象的分配根据对象大小的不同申请策略也不同:

  • 当要分配大于 32K 的对象时,从 mheap 分配。
  • 当要分配的对象小于等于 32K 且大于 16B 时,从 P 上的 mcache 分配;如果 mcache 没有内存,则从 mcentral 获取;如果 mcentral 也没有,则向 mheap 申请;如果 mheap 也没有,则从操作系统申请内存。
  • 当要分配的对象小于等于 16B 时(微小对象),从 mcache 上的微型分配器上分配。

4. Channel 分配在栈上还是堆上?哪些对象分配在堆上,哪些对象分配在栈上?
#

channel 分配在栈上还是堆上?
#

分析

这个题可以看作是对上一个题理解程度的考察,可以用内存逃逸的思想来分析这个题。因为 channel 的作用就是用做两个 goroutine 之间的通信,所以在很大程度上它的生命周期并不会局限在一个函数内部,所以大概率会发生内存逃逸,就很容易得出结论:channel 大概率会分配到堆上。

回答

channel 分配在堆上。Channel 被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以 Golang 直接将其分配在堆上。

哪些对象分配在堆上,哪些对象分配在栈上?
#

分析

对象是分配在栈上还是堆上跟 Go 语言的语法没有关系,需要在编译期由编译器进行逃逸分析而决定。根据逃逸分析策略来思考。

回答

一般而言,大的对象直接分配在堆上。如果一个局部变量会被外部引用,生命周期不确定,也会分配到堆上。其他小对象会优先分配在栈上。

5. 介绍一下大对象小对象,什么情况下会导致 GC 压力大?
#

分析

首先 GC 压力大指的是我们 GC 的时候,需要占用比较高的 CPU 时间和内存带宽资源,这就会影响用户 Goroutine 的执行。

而当大量小对象逃逸到堆上,就意味着这些小对象是需要被 GC 回收的(可能是在某一次 GC)。因为栈上的对象可以随着栈帧的释放而回收,而堆上的对象只能由 GC 来进行内存管理。

同样巨大的 mapslice 也就意味着 GC 需要处理更多的数据。

回答

  • Go 语言中小于等于 32K 的对象就是小对象,其中小于 16B 的是微小对象(而且是不含指针的对象),其它都是大对象。
  • 当有大量小对象逃逸到堆上,或者有巨大的元素类型为指针的 mapslice 的情况下,GC 压力会较大。
Golang面试题库 - 这篇文章属于一个选集。
§ 11: 本文