Golang内存泄漏场景以及解决方案详析

 更新时间:2023年01月10日 10:02:12   作者:8023之永恒  
golang中内存泄露的发现与排查一直是来是go开发者头疼的一件事,下面这篇文章主要给大家介绍了关于Golang内存泄漏场景以及解决的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

1、字符串截取

func main() {
	var str0 = "12345678901234567890"
	str1 := str0[:10]
}

以上代码,会有10字节的内存泄漏,我们知道,str0和str1底层共享内存,只要str1一直活跃,str0 就不会被回收,10字节的内存被使用,剩下的10字节内存就造成了临时性的内存泄漏,直到str1不再活跃

如果str0足够大,str1截取足够小,或者在高并发场景中频繁使用,那么可想而知,会造成临时性内存泄漏,对性能产生极大影响。

解决方案1:string to []byte, []byte to string

func main() {
	var str0 = "12345678901234567890"
	str1 := string([]byte(str0[:10]))
}

将需要截取的部分先转换成[]byte,再转换成string,但是这种方式会产生两个10字节的临时变量,string转换[]byte时产生一个10字节临时变量,[]byte转换string时产生一个10字节的临时变量

解决方案2

func main() {
	var str0 = "12345678901234567890"
	str1 := (" " + str0[:10])[1:]
}

这种方式仍旧会产生1字节的浪费 

解决方案3:strings.Builder

func main() {
	var str0 = "12345678901234567890"
	var builder strings.Builder
	builder.Grow(10)
	builder.WriteString(str0[:10])
	str1 := builder.String()
}

这种方式的缺点就是代码量过多

解决方案4:strings.Repeat

func main() {
	var str0 = "12345678901234567890"
	str1 := strings.Repeat(str0[:10], 1)
}

这种方式底层还是用到了strings.Builder,优点就是将方案3进行了封装,代码量得到了精简

2、切片截取引起子切片内存泄漏

func main() {
	var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := s0[:5]
}

这种情况与字符串截取引起的内存泄漏情况类似,s1活跃情况下,造成s0中部分内存泄漏

解决方案:append

func main() {
	var s0 = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	s1 := append(s0[:0:0], s0[:5]...)
}

 append为内置函数,go源码src/builtin/builtin.go中释义:

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//	slice = append(slice, elem1, elem2)
//	slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//	slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

3、没有重置丢失的子切片元素中的指针

func main() {
	var s0 = []*int{new(int), new(int), new(int), new(int), new(int)}
	s1 := s0[1:3]
}

原切片元素为指针类型,原切片被截取后,丢失的子切片元素中的指针元素未被置空,导致内存泄漏

解决方案:元素置空 

func main() {
	var s0 = []*int{new(int), new(int), new(int), new(int), new(int)}
	s0[0], s0[3], s0[4] = nil, nil, nil
	s1 := s0[1:3]
}

4、函数数组传参

Go数组是值类型,赋值和函数传参都会复制整个数组

func main() {
	var arrayA = [3]int{1, 2, 3}
	var arrayB = [3]int{}
	arrayB = arrayA
	fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA)
	fmt.Printf("arrayB address: %p, arrayB value: %+v\n", &arrayB, arrayB)
	array(arrayA)
}
 
func array(array [3]int) {
	fmt.Printf("array address: %p, array value: %+v\n", &array, array)
}

 打印结果:

arrayA address: 0xc0000ae588, arrayA value: [1 2 3]
arrayB address: 0xc0000ae5a0, arrayB value: [1 2 3]
array address: 0xc0000ae5e8, array value: [1 2 3]

可以看到,三条打印的地址都不相同,说明数组是值传递的,这会导致什么问题呢?

如果我们在函数传参的时候用到了数组传参,且这个数组够大(我们假设数组大小为100万,64位机上消耗的内存约为800w字节,即8MB内存),或者该函数短时间内被调用N次,那么可想而知,会消耗大量内存,对性能产生极大的影响,如果短时间内分配大量内存,而又来不及GC,那么就会产生临时性的内存泄漏,对于高并发场景相当可怕。

解决方案1:采用指针传递

func main() {
	var arrayA = [3]int{1, 2, 3}
	var arrayB = &arrayA
	fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA)
	fmt.Printf("arrayB address: %p, arrayB value: %+v\n", arrayB, *arrayB)
	arrayP(&arrayA)
}
 
func arrayP(array *[3]int) {
	fmt.Printf("array address: %p, array value: %+v\n", array, *array)
}

打印结果: 

arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc00000e6a8, arrayB value: [1 2 3]
array address: 0xc00000e6a8, array value: [1 2 3]

可以看到,三条打印的地址相同,说明指针是引用传递的 ,三个数组指向的都是同一块内存,就算数组很大,或者函数短时间被调用N次,也不会产生额外的内存开销,这样会不会有隐患呢?

有,如果arrayA的指针地址发生变化,那么,arrayB和函数内array的指针地址也随之改变,稍不注意,容易发生bug

解决方案2:利用切片可以很好的解决以上两个问题

func main() {
	var arrayA = [3]int{1, 2, 3}
	var arrayB = arrayA[:]
	fmt.Printf("arrayA address: %p, arrayA value: %+v\n", &arrayA, arrayA)
	fmt.Printf("arrayB address: %p, arrayB value: %+v\n", &arrayB, arrayB)
	arrayS(arrayB)
}
 
func arrayS(array []int) {
	fmt.Printf("array address: %p, array value: %+v\n", &array, array)
}

打印结果:

arrayA address: 0xc00000e6a8, arrayA value: [1 2 3]
arrayB address: 0xc0000040d8, arrayB value: [1 2 3]
array address: 0xc000004108, array value: [1 2 3]

 可以看到,三条打印的地址都不相同,而切片本身是一个引用类型,arrayA和arrayB底层共享内存,不会产生额外内存开销,而且arrayA的指针地址发生改变,arrayB的指针地址也不会改变,切片的数据结构如下:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

5、goroutine

“Go里面10次内存泄漏有9次都是goroutine泄漏引起的”

有些编码不当的情况下,goroutine被长期挂住,导致该协程中的内存也无法被释放,就会造成永久性的内存泄漏。例如协程结束时协程中的channel没有关闭,导致一直阻塞;例如协程中有死循环;等等

我们来看下

func main() {
	ticker := time.NewTicker(time.Second * 1)
	for {
		<-ticker.C
		ch := make(chan int)
		go func() {
			for i := 0; i < 100; i++ {
				ch <- i
			}
		}()
 
		for v := range ch {
			if v == 50 {
				break
			}
		}
	}
}

将代码运行起来,并利用pprof工具,在web输入http://localhost/debug/pprof/,我们可以看到,goroutine的数量随着时间在不断的增加,而且丝毫没有减少的迹象

 这是因为break的时候,协程中的channel并没有关闭,导致协程一直存活,无法被回收

解决方案:

func main() {
	ticker := time.NewTicker(time.Second * 1)
	for {
		<-ticker.C
		cxt, cancel := context.WithCancel(context.Background())
		ch := make(chan int)
		go func(cxt context.Context) {
			for i := 0; i < 100; i++ {
				select {
				case <-cxt.Done():
					return
				case ch <- i:
				}
			}
		}(cxt)
 
		for v := range ch {
			if v == 50 {
				cancel()
				break
			}
		}
	}
}

利用context,在break之前cancel,目的就是通知协程退出,这样就避免了goroutine泄漏 

6、定时器

1)time.After

func main() {
	ch := make(chan int)
	go func() {
		for {
			timerC := time.After(100 * time.Second)
			//timerC 每次都是重新创建的,什么意思呢?简单说来,当 select 成功监听 ch 并进入它的处理分支,下次循环 timerC 重新创建了,时间肯定就重置了。
			select {
			//如果有多个 case 都可以运行,select 会随机公平选择出一个执行。其余的则不会执行
			case num := <-ch:
				fmt.Println("get num is", num)
			case <-timerC:
				//等价于 case <-time.After(100 * time.Second)
				fmt.Println("time's up!!!")
				//done<-true
			}
		}
	}()
 
	for i := 1; i < 100000; i++ {
		ch <- i
		time.Sleep(time.Millisecond)
	}
}

 以上代码会造成内存泄漏,time.After底层实现是一个timer,而定时器未到触发时间,该定时器不会被gc回收,从而导致临时性的内存泄漏,而如果定时器一直在创建,那么就造成了永久性的内存泄漏了。

解决方案:采用timer定时器

func main() {
	ch := make(chan int)
	go func() {
		timer := time.NewTimer(100 * time.Second)
		defer timer.Stop()
		for {
			timer.Reset(100 * time.Second)
			select {
			case num := <-ch:
				fmt.Println("get num is", num)
			case <-timer.C:
				fmt.Println("time's up!!!")
			}
		}
	}()
 
	for i := 1; i < 100000; i++ {
		ch <- i
		time.Sleep(time.Millisecond)
	}
}

 创建timer定时器,每次需要启动定时器的时候,使用Reset方法重置定时器,这样就不用每次都要创建新的定时器了

2)timer、ticker

在高并发、高性能场景中,使用time.NewTimer或者time.NewTicker定时器,都需要注意及时调用Stop方法来及时释放资源,否则可能造成临时性或者永久性的内存泄漏。

总结

到此这篇关于Golang内存泄漏场景以及解决方案的文章就介绍到这了,更多相关Golang内存泄漏场景及解决内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang频率限制 rate详解

    golang频率限制 rate详解

    这篇文章主要介绍了golang频率限制 rate详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • GoLang桥接模式的实现示例

    GoLang桥接模式的实现示例

    桥接模式是一种结构型设计模式,通过桥接模式可以将抽象部分和它的实现部分分离,本文主要介绍了GoLang桥接模式,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Golang极简入门教程(二):方法和接口

    Golang极简入门教程(二):方法和接口

    这篇文章主要介绍了Golang极简入门教程(二):方法和接口,本文同时讲解了错误、匿名域等内容,需要的朋友可以参考下
    2014-10-10
  • Windows下安装VScode 并使用及中文配置方法

    Windows下安装VScode 并使用及中文配置方法

    这篇文章主要介绍了Windows下安装VScode 并使用及中文配置的方法详解,本文通过图文并茂的形式给大家介绍,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • GO实现跳跃表的示例详解

    GO实现跳跃表的示例详解

    跳表全称叫做跳跃表,简称跳表,是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。本文将利用GO语言编写一个跳表,需要的可以参考一下
    2022-12-12
  • go xorm存库处理null值问题

    go xorm存库处理null值问题

    这篇文章主要介绍了go xorm存库处理null值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • 7分钟读懂Go的临时对象池pool以及其应用场景

    7分钟读懂Go的临时对象池pool以及其应用场景

    这篇文章主要给大家介绍了关于如何通过7分钟读懂Go的临时对象池pool以及其应用场景的相关资料,文中通过示例代码介绍的非常详细,对大家学习或使用Go具有一定的参考学习价值,需要的朋友们下面来一起看看吧
    2018-11-11
  • 大多数Go程序员都走过的坑盘点解析

    大多数Go程序员都走过的坑盘点解析

    这篇文章主要为大家介绍了大多数Go程序员都走过的坑盘点解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • 详解Golang Iris框架的基本使用

    详解Golang Iris框架的基本使用

    这篇文章主要介绍了Golang Iris框架的基本使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2020-11-11
  • 为什么Go里值为nil可以调用函数原理分析

    为什么Go里值为nil可以调用函数原理分析

    这篇文章主要为大家介绍了为什么Go里值为nil可以调用函数原理分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08

最新评论