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 语言的逃逸分析是在编译期决定对象分配方式的一种策略,决定对象时分配在堆上还是分配在栈上。
就是优先本来可以分配到栈上的对象被分配到堆上了,这个时候就发生了逃逸。
具体的逃逸分析策略有以下几种:
- 如果函数外部没有引用,则优先放到栈中。
- 如果函数外部存在引用,则必定放到堆中。
- 如果栈上放不下,则必定放到堆上。
前两种主要考虑对象的生命周期,当这个对象的生命周期不能被确定,不能跟随当前函数结束而结束,就会发生逃逸,被分配到堆上。
回答
内存逃逸是编译器在程序编译时期根据逃逸分析策略,将原本应该分配到栈上的对象分配到堆上的一个过程。
什么情况下会发生内存逃逸#
以下场景会发生内存逃逸:
- 方法内返回局部变量指针。
- 向
channel发送指针数据。 - 在闭包中引用包外的值。
- 在
slice或map中存储指针。 - 切片(扩容后)长度太大。
- 在
interface类型上调用方法。
内存逃逸有什么影响#
分析
这个问题可以从栈对象和堆对象的区别来回答。堆对象需要垃圾回收机制来释放内存,栈对象会跟随函数结束被编译器回收。
回答
大量的内存逃逸会给 GC 带来压力。
3. 请简述 Go 是如何分配内存的?#
分析
Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。
具体的分级策略回顾内存模型图:

回答
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 来进行内存管理。
同样巨大的 map 和 slice 也就意味着 GC 需要处理更多的数据。
回答
- Go 语言中小于等于
32K的对象就是小对象,其中小于16B的是微小对象(而且是不含指针的对象),其它都是大对象。 - 当有大量小对象逃逸到堆上,或者有巨大的元素类型为指针的
map和slice的情况下,GC 压力会较大。