大纲
文章将从一下几个点去知识内容介绍,先看大纲做了解,再看内容。
前言知识
1:什么是内存泄漏
内存泄漏(Memory Leak)并不是指物理上的内存消失,而是在写程序的过程中,由于程序的设计不合理导致对之前使用的内存失去控制,无法再利用这块内存区域,程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
如果发现程序内存泄漏了,解决起来也很简单退出程序,然后重启,不过对于对外提供服务的程序,肯定是不能接受的,那将导致服务中断,这是非常严重的事故。
2:垃圾回收GC
我们知道Golang垃圾回收 (GC garbage collection) 是一种自动内存管理机制,即我们在程序中定义一个变量后,会在内存中开辟相应空间进行存储。当不需要此变量后,需要手动销毁此对象,并释放内存, 而这种对不再使用的内存资源进行自动回收的功能即为垃圾回收,那么为什么还会出现内存泄漏呢?
因为过程中如果不注意,很容易造成内存泄漏的问题。比较常见的是发生在 slice、time.Ticker、goroutine 等的使用过程中,本文将从Golang内存泄漏的一些常见场景来看内存泄漏,然后学习如何避免和排查。
内存泄漏场景
1:slice
下面这段代码很多人会觉得没问题,我们知道slice底层有一个指向数组的指针地址,当两个slice 共享地址(同一个底层数组),其中一个为全局变量,另一个也无法被GC。推荐:煎鱼大佬对这个场景的分析
var a []int
func test(b []int) {
a = b[:3]
return
}
解决这个问题可以使用append的方法,append不会直接引用原来的数组,而是会新申请内存来存放数据。
2:time.After、 time.NewTicker
time.After通过函数的注释可以知道,每次time.After(duration x)会产生NewTimer(),在duration x到期之前,新创建的timer不会被GC,到期之后才会GC,NewTimer()返回一个Timer到只读channel中。
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1)
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
time.After 泄漏场景示例代码,我们来看看为什么会导致内存泄漏的发生,NewTimer(d).C 每次都是 return 了一个新的对象。并且我们是在 for 循环中定时执行 select,也就相当于每一次执行 select 我们都重新创建(实例化)了新的 time.After(),因此每一次执行 select time.After() 都会重新开始计时,只有到期之后新建的。
for true {
select {
case <-time.After(time.Minute * 5):
// do something
default:
time.Sleep(time.Duration(1) * time.Second)
}
}
time.After上述场景可以使用NewTimer()或者NewTicker()代替的方式主动释放资源。
//这两种情况下的idleDuration都可以
//idleDuration := time.NewTimer(time.Second * 5).C
idleDuration := time.After(time.Second * 5)
ticker := time.NewTicker(time.Second * 1)
for {
select {
case <-ticker.C:
fmt.Println("xiaoxu")
case <-idleDuration:
fmt.Println("coding")
return
}
}
}()
time.NewTicker使用如下,是需要用defer Stop()去主动释放资源的,否则造成内存泄漏。
timer := time.NewTicker(time.Duration(2) * time.Second)
defer timer.Stop() // 主动释放资源
for true {
select {
case <-timer.C:
// do something
default:
time.Sleep(time.Duration(1) * time.Second)
}
}
3:goroutine泄漏
关于Go的内存泄漏有这么一句话:10次内存泄漏,有9次是goroutine泄漏。我们启动一个goroutine非常简单,如果没有按预期退出,直到程序退出时goroutine才退出,goroutine就泄漏了,goroutine泄漏的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放.
一般有以下几种情况:
不说废话了,接下来一个个举例介绍这些场景
1:从channel当中读,但是没有goroutine写入channel
func leakNoWrite() {
ch := make(chan int)
go func() {
val := <- ch
}()
}
2:向无缓冲channel写,没有读操作而阻塞
func leakNoRead() {
ch := make(chan bool)
go func() {
ch <- true
}()
}
3:向已满有缓冲channel写入,没有读操作而阻塞
func leakWriteToFullBuffer() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
}
// fatal error: all goroutines are asleep - deadlock!
4:select-case
select时case上没有完全覆盖所有场景也就是case操作阻塞,导致这个goroutine不能退出,最终发生内存泄漏。
func task() {
for {
select {
//case <-exit:
// return
case <-other:
//do task
}
}
}
func main() {
go task()
}
for循环条件一旦命中default则会出现循环空转的情况,并最终导致资源无法释放
msg := make(chan int, 10)
go func() {
for {
select {
case <-msg:
default:
}
}
}()
定位和排查
我们现在很多服务都是上云的,很多问题在云平台的监控系统都能很清晰的展示出来,特别是goroutine泄漏,一个是goroutine数量的持续增加不释放,内存持续增长等情况。当然除了云平台我们也可以使用Go语言本身的工具pprof,具体如何使用和排查相关的文章很多,这里就不做过多介绍了。
其实最主要的还是如何避免,规范写法,多学多总结经验,在开发的时候就把这个问题从根源上尽量避免,出现问题再排查总会更耗时耗力。