大纲

文章将从一下几个点去知识内容介绍,先看大纲做了解,再看内容。

前言知识

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,具体如何使用和排查相关的文章很多,这里就不做过多介绍了。

其实最主要的还是如何避免,规范写法,多学多总结经验,在开发的时候就把这个问题从根源上尽量避免,出现问题再排查总会更耗时耗力。