什么是内存泄漏?
预期很快能被释放的内存由于某些原因生命周期意外地被延长,导致预计能够立即回收的内存却长时间得不到回收,这就是内存泄漏。
Go语言中内存泄漏的原因
- goruntine(协程)导致的内存泄漏(最主要的原因)
go语言中协程的创建非常简单,导致我们很多时候只关心协程的代码功能实现,而忽略了协程什么时候结束退出的问题。如果协程在执行时被阻塞而无法退出,就会导致协程的内存泄漏。一个协程的最少占用为2k的内存,在高并发场景下,如果存在大量的协程内存泄漏,则会导致系统严重的内存泄漏。
func finishReq(timeout time.Duration) r ob{
ch := make(chan ob)
// ch := make(chan ob,1)
go func() {
result := fn()
ch <- result
}()
select {
case result = <- ch:
return result
case <- time.After(timeout):
return nil
}
}
finishRequest函数在第4行使用匿名函数创建子goroutine来处理请求,这是Go服务器程序中的常见做法。子goroutine执行fn(),并通过第6行的通道ch将结果发送回父goroutine。子协程将在第6行阻塞,直到父级从第9行的ch中提取结果。同时,父级将在select时阻塞,直到子级将结果发送给ch(第9行)或发生超时(第11行)。如果超时提前发生,或者如果Go运行时(非确定性)在两个情况都有效的情况下选择了第11行的情况,则父级将从第12行的requestReq()返回,其他任何人都不能再从ch中提取结果,导致子协程永远被阻止。修复方法是将ch从非缓冲通道更改为缓冲通道,这样子goroutine即使在父级退出时也可以始终发送结果。
- 互斥锁未被释放,导致内存泄漏
- time.Ticker是每间隔指定的时间就会向通道内写数据。作为循环触发器,必须调用stop方法才会停止,从而才能被GC回收,否则将会一直占用内存空间;
- 字符串的截取而引发的内存泄漏。一个长的字符串被另一个字符串通过切片的方式截取,这两个字符串共用一个底层数组。如果截取的字符串很小,但是原来的字符串很大,只要截取的小字符串还在活跃,则大的字符串将不会被回收,这样会造成临时的内存泄漏;
- 同理切片的截取也会存在这样的情况。
Go发现内存泄漏的两种方法:
- 一个是通用的监控工具,如果在监控工具中发现随着时间的推进,内存的占用率在不断的提高,这就是内存泄漏最明显的现象。
- 另一个是go pprof,pprof是性能分析工具,在程序运行过程中可以记录程序的运行信息,CPU使用情况,内存使用情况、协程运行情况等。