一篇文章讲清楚golang内存泄漏,Go必知必会,6分钟
- 什么是"内存泄漏"?
内存泄漏并不是指物理上的内存消失,而是在写程序的过程中,由于程序的设计不合理导致对之前使用的内存失去控制,无法再利用这块内存区域;短期内的内存泄漏可能看不出什么影响,但是当时间长了之后,日积月累,浪费的内存越来越多,导致可用的内存空间减少,轻则影响程序性能,严重可导致正在运行的程序突然崩溃。
一般一个进程结束之后,内存会自动回收,同时也会自动回收那些被泄露的内存,当进程重新启动后,这些内存又可以重新被分配使用。但是正常情况下企业的程序是不会经常重启的,所以最好的办法就是从源头上解决内存泄漏的问题。
go虽然是自动GC类型的语言,但在书写过程中如果不注意,很容易造成内存泄漏的问题。比较常见的是发生在 slice、time.Ticker、goroutine 等的使用过程中。
2.那些情况会产生内存泄漏?
slice 引起的内存泄漏
Golang是自带GC的,如果资源一直被占用,是不会被自动释放的,比如下面的代码,如果传入的slice b是很大的,然后引用很小部分给全局量a,那么b未被引用的部分就不会被释放,造成了所谓的内存泄漏。
var a []int func test(b []int) { a = b[:3] return }
想要理解这个内存泄漏,主要就是理解上面的a = b[:3]是一个引用,其实新、旧slice指向的都是同一片内存地址,那么只要全局量a在,b就不会被回收。
数组的值传递
由于数组是Golang的基本数据类型,每个数组占用不同的内存空间,生命周期互不干扰,很难出现内存泄漏的情况,但是数组作为形参传输时,遵循的是值拷贝,如果函数被多个goroutine调用且数组过大时,则会导致内存使用激增。
//统计nums中target出现的次数 func countTarget(nums [1000000]int, target int) int { num := 0 for i := 0; i < len(nums) && nums[i] == target; i++ { num++ } return num }
因此对于大数组放在形参场景下通常使用切片或者指针进行传递,避免短时间的内存使用激增。
select阻塞
使用select时如果有case没有覆盖完全的情况且没有default分支进行处理,最终会导致内存泄漏
定时器使用不当
1.time.After()的使用
默认的time.After()是会有内存泄露问题的,因为每次time.After(duration x)会产生NewTimer(),在duration x到期之前,新创建的timer不会被GC,到期之后才会GC。
随着时间推移,尤其是duration x很大的话,会产生内存泄露的问题,应特别注意
for true { select { case <-time.After(time.Minute * 3): // do something default: time.Sleep(time.Duration(1) * time.Second) } }
为了保险起见,使用NewTimer()或者NewTicker()代替的方式主动释放资源,
//两者区别参考文章: https://blog.csdn.net/weixin_38299404/article/details/119352884
2.time.NewTicker资源未及时释放
在使用time.NewTicker时需要手动调用Stop()方法释放资源,否则将会造成永久性的内存泄漏
channel阻塞
channel阻塞主要分为写阻塞和读阻塞两种情况
空channel
func channelTest() { //声明未初始化的channel读写都会阻塞 var c chan int //向channel中写数据 go func() { c <- 1 fmt.Println("g1 send succeed") time.Sleep(1 * time.Second) }() //从channel中读数据 go func() { <-c fmt.Println("g2 receive succeed") time.Sleep(1 * time.Second) }() time.Sleep(10 * time.Second) }
写阻塞:
1.无缓冲channel的阻塞通常是写操作因为没有读而阻塞
func channelTest() { var c = make(chan int) //10个协程向channel中写数据 for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1个协程丛channel读数据 go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //会有写的9个协程阻塞得不到释放 time.Sleep(10 * time.Second) }
2.有缓冲的channel因为缓冲区满了,写操作阻塞
func channelTest() { var c = make(chan int, 8) //10个协程向channel中写数据 for i := 0; i < 10; i++ { go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() } //1个协程丛channel读数据 go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() //会有写的几个协程阻塞写不进去 time.Sleep(10 * time.Second) }
读阻塞:
期待从channel读数据,结果没有goroutine往进写数据
func channelTest() { var c = make(chan int) //1个协程向channel中写数据 go func() { <- c fmt.Println("g1 receive succeed") time.Sleep(1 * time.Second) }() //10个协程丛channel读数据 for i := 0; i < 10; i++ { go func() { c <- 1 fmt.Println("g2 send succeed") time.Sleep(1 * time.Second) }() } //会有读的9个协程阻塞得不到释放 time.Sleep(10 * time.Second) }
goroutine导致的内存泄漏
- 申请过多的goroutine
例如在for循环中申请过多的goroutine来不及释放导致内存泄漏
2. goroutine阻塞
3. I/O问题
I/O连接未设置超时时间,导致goroutine一直在等待,代码会一直阻塞。
4.互斥锁未释放
goroutine无法获取到锁资源,导致goroutine阻塞
//协程拿到锁未释放,其他协程获取锁会阻塞 func mutexTest() { mutex := sync.Mutex{} for i := 0; i < 10; i++ { go func() { mutex.Lock() fmt.Printf("%d goroutine get mutex", i) //模拟实际开发中的操作耗时 time.Sleep(100 * time.Millisecond) }() } time.Sleep(10 * time.Second) }
5.死锁
当程序死锁时其他goroutine也会阻塞
func mutexTest() { m1, m2 := sync.Mutex{}, sync.RWMutex{} //g1得到锁1去获取锁2 go func() { m1.Lock() fmt.Println("g1 get m1") time.Sleep(1 * time.Second) m2.Lock() fmt.Println("g1 get m2") }() //g2得到锁2去获取锁1 go func() { m2.Lock() fmt.Println("g2 get m2") time.Sleep(1 * time.Second) m1.Lock() fmt.Println("g2 get m1") }() //其余协程获取锁都会失败 go func() { m1.Lock() fmt.Println("g3 get m1") }() time.Sleep(10 * time.Second) }
6. waitgroup使用不当
waitgroup的Add、Done和wait数量不匹配会导致wait一直在等待。
如何排查内存泄露?
1. 单个函数,调用runtime.NumGoroutine()放大打印执行代码前后协程的运行数量
2. 生产环境/测试环境:使用pprof实时监控协程的数量
pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。
基本使用
GO中已经封装好了,直接就能使用
_"net/http/pprof"
package main import ( "fmt" "net/http" _ "net/http/pprof" ) func main() { // 开启pprof,监听请求 ip := "127.0.0.1:6069" if err := http.ListenAndServe(ip, nil); err != nil { fmt.Printf("start pprof failed on %s\n", ip) } }
浏览器查看
浏览器访问就能看到效果
http://127.0.0.1:6069/debug/pprof/