内存泄漏并不是指物理上的内存消失,而是在写程序的过程中,由于程序的设计不合理导致对之前使用的内存失去控制,无法再利用这块内存区域;短期内的内存泄漏可能看不出什么影响,但是当时间长了之后,日积月累,浪费的内存越来越多,导致可用的内存空间减少,轻则影响程序性能,严重可导致正在运行的程序突然崩溃。
一般一个进程结束之后,内存会自动回收,同时也会自动回收那些被泄露的内存,当进程重新启动后,这些内存又可以重新被分配使用。但是正常情况下企业的程序是不会经常重启的,所以最好的办法就是从源头上解决内存泄漏的问题。
go虽然是自动GC类型的语言,但在书写过程中如果不注意,很容易造成内存泄漏的问题。比较常见的是发生在 slice、time.Ticker、goroutine 等的使用过程中
我们知道,go语言默认是值传递类型,也就是赋值和函数传参操作都会复制整个数据,但有一些采用的是引用传递类型,比如slice、map、channel、interface等。没错,slice采用的就是引用传递类型,slice本身是一个只读对象,它通过指针引用底层数组,类似数组指针的一种封装。slice的结构定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量,而且 cap 总是大于等于 len 的。上面我们说到slice是通过指针引用底层数组的,如下图所示:
那这样设计有什么好处呢,相比于数组来说,切片的长度是可变的,在使用上更灵活,而且,前面也说到切片本质是对数组的引用,在传递过程中是引用传递,在传递大容量的切片时是可以节省空间的,只需要传递一个地址,但是正因为这一特性,也使得slice在使用不当的情况下会发生内存泄漏
如下是一个切片的使用案例
func TestSlice(t *testing.T) {
slice1 := []int{3, 4, 5, 6, 7}
slice2 := initSlice[1:3]
fmt.Printf("slice1 addr: %p", &slice1)
fmt.Println()
fmt.Printf("slice2 addr: %p", &slice2)
fmt.Println()
for i := 0; i < len(slice1); i++ {
fmt.Printf("%v:[%v] ", slice1[i], &slice1[i])
}
fmt.Println()
for i := 0; i < len(slice2); i++ {
fmt.Printf("%v:[%v] ", slice2[i], &slice2[i])
}
fmt.Println()
}
// output
// slice1 addr: 0xc00000c090
// slice2 addr: 0xc00000c0a8
// 3:[0xc00001e1b0] 4:[0xc00001e1b8] 5:[0xc00001e1c0] 6:[0xc00001e1c8] 7:[0xc00001e1d0]
// 4:[0xc00001e1b8] 5:[0xc00001e1c0]
从打印的地址可以看出,两个切片的地址是不一样的,但是里面的元素地址是一样的,如下图所示:
那么这里的内存泄漏主要体现在哪里呢?上面的代码是通过slice1去初始化了一个数组,然后slice1引用了这个数组,再然后是slice2只取了slice1中的一部分,也就是数组中的一部分。
可以采用append的方法,append不会直接引用原来的数组,而是会新申请内存来存放数据,这样
func TestSlice(t *testing.T) {
initSlice := []int{3, 4, 5, 6, 7}
//partSlice := initSlice[1:3]
var partSlice []int
partSlice = append(partSlice, initSlice[1:3]...) // append
fmt.Printf("initSlice addr: %p", &initSlice)
fmt.Println()
fmt.Printf("partSlice addr: %p", &partSlice)
fmt.Println()
for i := 0; i < len(initSlice); i++ {
fmt.Printf("%v:[%v] ", initSlice[i], &initSlice[i])
}
fmt.Println()
for i := 0; i < len(partSlice); i++ {
fmt.Printf("%v:[%v] ", partSlice[i], &partSlice[i])
}
fmt.Println()
}
// output
// initSlice addr: 0xc00011c078
// partSlice addr: 0xc00011c090
// 3:[0xc00012e030] 4:[0xc00012e038] 5:[0xc00012e040] 6:[0xc00012e048] 7:[0xc00012e050]
// 4:[0xc00010c1d0] 5:[0xc00010c1d8]
可见slice1和slice2相应位置上的内存地址是不一样的,即新开辟了内存空间
如下是使用copy代替直接切片的写法
func TestSlice(t *testing.T) {
initSlice := []int{3, 4, 5, 6, 7}
//partSlice := initSlice[1:3]
partSlice := make([]int, 2)
copy(partSlice, initSlice[1:3]) //copy
fmt.Printf("initSlice addr: %p", &initSlice)
fmt.Println()
fmt.Printf("partSlice addr: %p", &partSlice)
fmt.Println()
for i := 0; i < len(initSlice); i++ {
fmt.Printf("%v:[%v] ", initSlice[i], &initSlice[i])
}
fmt.Println()
for i := 0; i < len(partSlice); i++ {
fmt.Printf("%v:[%v] ", partSlice[i], &partSlice[i])
}
fmt.Println()
}
// output
// initSlice addr: 0xc0000b0078
// partSlice addr: 0xc0000b0090
// 3:[0xc0000a6060] 4:[0xc0000a6068] 5:[0xc0000a6070] 6:[0xc0000a6078] 7:[0xc0000a6080]
// 4:[0xc0000b81c0] 5:[0xc0000b81c8]
可见slice1和slice2相应位置上的内存地址是不一样的,即新开辟了内存空间
go语言的time.Ticker主要用来实现定时任务,time.NewTicker(duration) 可以初始化一个定时任务,里面填写的时间长度duration就是指每隔 duration 时间长度就会发送一次值,可以在 ticker.C 接收到,这里容易造成内存泄漏的地方主要在于编写代码过程中没有stop掉这个定时任务,导致定时任务一直在发送,从而导致内存泄漏
如下是一个错误的案例:
func TestTicker(t *testing.T) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop() // 这个stop一定不能漏了
go func(ticker *time.Ticker) {
for {
select {
case value := <-ticker.C:
fmt.Println(value)
}
}
}(ticker)
time.Sleep(time.Second * 5)
fmt.Println("finish!!!")
}
// output
// 2022-09-25 18:26:14.389209 +0800 CST m=+1.002042233
// 2022-09-25 18:26:15.388206 +0800 CST m=+2.001142653
// 2022-09-25 18:26:16.388425 +0800 CST m=+3.001458610
// 2022-09-25 18:26:17.388717 +0800 CST m=+4.001840387
// finish!!!
解决办法就是不要忘记stop ticker
在平时开发过程中,goroutine泄漏通常是最常见也最频繁的,goroutine是协程,本身占用内存不大,一般就2KB只有,但是当goroutine开的数量多了之后,如果处理不当导致内存泄漏,一样会对服务造成严重问题
提到goroutine,一般都是和channel配合使用的,关于channel的介绍可以看我之前写的一篇文章: Title Golang中的channel解析与实战
总体来说,goroutine泄漏一般可分为如下几种情况:
仍然向满了的channel发送消息,导致了阻塞,从而导致内存泄漏,如下是无缓存channel的案例:
func TestSend(t *testing.T) {
ch := make(chan int)
fmt.Println("num of go start: ", runtime.NumGoroutine())
time.Sleep(time.Second)
for i := 0; i < 5; i++ { // 向channel发送5次
go func(ii int) {
ch <- ii
fmt.Println("send to chan: ", ii)
}(i)
}
go func() { // 只从channel接收一次
value := <-ch
fmt.Println("recv from chan: ", value)
}()
time.Sleep(time.Second)
fmt.Println("num of go end: ", runtime.NumGoroutine())
}
// output
// num of go start: 2
// recv from chan: 0
// send to chan: 0
// num of go end: 6
由结果可以看出结束的时候goroutine的数量比开始的时候多了4个,而且不管运行多少次都是这个结果,这4个goroutine就会造成内存泄漏,因为channel只被接收了1次,但是向channel发送了5次,其中4goroutine个都被阻塞了,如果这4个goroutine没有被接收,那么就会一直阻塞直到程序结束,内存在这期间就被浪费了
现在初始化一个缓存为2的channel
func TestSend(t *testing.T) {
ch := make(chan int, 2)
fmt.Println("num of go start: ", runtime.NumGoroutine())
time.Sleep(time.Second)
for i := 0; i < 5; i++ {
go func(ii int) {
ch <- ii
fmt.Println("send to chan: ", ii)
}(i)
}
go func() {
value := <-ch
fmt.Println("recv from chan: ", value)
}()
time.Sleep(time.Second)
fmt.Println("num of go end: ", runtime.NumGoroutine())
}
// output
// num of go start: 2
// send to chan: 0
// send to chan: 1
// recv from chan: 0
// send to chan: 2
// num of go end: 4
由运行结果可知,运行结束后多了2个goroutine,即造成了2个goroutine泄漏;这次的channel缓存为2,所以有2个goroutine发送的消息放到了缓存中,所以最后的goroutine个数才会比无缓存的案例少了2个
从空的channel接收,导致了阻塞,从而导致内存泄漏,如下是案例:
func TestRecv(t *testing.T) {
ch := make(chan int)
fmt.Println("num of go start: ", runtime.NumGoroutine())
time.Sleep(time.Second)
go func() {
ch <- 1
fmt.Println("send to chan")
}()
for i := 0; i < 5; i++ {
go func() {
value := <-ch
fmt.Println("recv from chan: ", value)
}()
}
time.Sleep(time.Second)
fmt.Println("num of go end: ", runtime.NumGoroutine())
}
// output
// num of go start: 2
// recv from chan: 1
// send to chan
// num of go end: 6
由结果可知结束的时候多了4个goroutine,即泄漏了4个goroutine
当channel没有初始化的时候就会处于nil状态,如下例:
func TestNil(t *testing.T) {
var ch chan int // 只命名而不通过make初始化
fmt.Println("num of go start: ", runtime.NumGoroutine())
time.Sleep(time.Second)
go func() {
ch <- 1
fmt.Println("send to chan")
}()
go func() {
value := <-ch
fmt.Println("recv from chan", value)
}()
time.Sleep(time.Second)
fmt.Println("num of go end: ", runtime.NumGoroutine())
}
// output
// num of go start: 2
// num of go end: 4
由结果可知,运行结束时多了2个goroutine,即造成了2个goroutine泄漏,send to chan
和recv from chan
都没有打印,因为ch没有初始化,处于nil状态
发生泄漏前
发送者和接收者的数量最好要一致,channel记得初始化,不给程序发生内存泄漏的机会
发生泄漏后
采用go tool pprof分析内存的占用和变化,细节不在本篇文章讲解
https://gfw.go101.org/article/memory-leaking.html
https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/Slice%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0.html