每日一课,不论长短,有所学有所得

业精于勤技在专,行则将至事必成

给大家分享一些书籍,希望能够提供一些帮助

golang语言的一大优势就是并发处理的简单,但是如果我们使用不当或者对其掌握不够,最常会头疼的一个问题就是内存泄漏;

虽然golang的协程占用资源少,但如果协程很多,且未得到释放,日积月累,时间长了就会发现内存溢出了,而这种问题,如果我们不了解正确的协程使用的方法或者我们没有好的监控方法的话,我们是很难发现问题的。

因此,我将整理两个思路分享给大家:

一是预防,了解哪些情况会导致内存溢出,避免潜在的风险;

二是监控,使用工具排查内存溢出的原因;

因为内容过多,这个话题我们将分两篇文章来介绍。

先上一张知识点大纲图预览:

内存泄露的场景

  1. 暂时性内存泄露

  2. 1. 子切片导致内存溢出:获取slice中的一段导致长数组无法释放

    • 底层是数组,只要数组中还存在引用,就无法释放该数组;因此不要直接截取slice使用,应该创建一个新slice,通过append的方法读取原slice中你需要用到的那部分数据,然后将原slice释放掉(如果原slice很大且不需要再使用的话)

    2. 在长slice中新建slice导致内存泄露

    3. 子字符串导致内存溢出:获取字符串中一段导致长字符串无法释放

    • 理由如第一种情况,string相当于一个只读的slice类型,因为截取string中的一段使用,使用结束之前,原string都不会被释放

  3. 永久性内存泄露              

  4. 1. goroutine泄漏

    • 处于永久阻塞状态的协程,资源无法释放,内存泄露

    2. time.Ticker未关闭导致泄漏

    • 对于不再使用的time.Ticker,必须调用stop方法结束,否则永远不会被释放

    3. finalizer导致泄漏

    • finalizer终结器的错误使用将导致内存泄露

    4. defer导致泄漏

    • 获取比如数据库连接,打开文件等资源后,使用defer及时释放可以避免内存泄露,但是defer存在额外的开销,如果在循环中使用不当,会导致占用大量资源

以上解释性demo:

// 1.1 子切片导致内存溢出
var s0 []int

func g(s1 []int) {
  s0 = s1[len(s1)-30:]
   //当函数g被调用之后,承载着切片s1的元素的内存块的开头大段内存将不再可用(假设没有其它值引用着此内存块)。
   //同时因为s0仍在引用着此内存块,所以此内存块得不到释放。
}

//想防止这样的临时性内存泄露,我们必须在函数g中将30个元素均复制一份,使得切片s0和s1不共享承载底层元素的内存块。
func g(s1 []int) {
  s0 = make([]int, 30)
  copy(s0, s1[len(s1)-30:])
  // 现在,如果再没有其它值引用着承载着s1元素的内存块,
  // 则此内存块可以被回收了。
}
// 1.2 slice中创建slice
func h() []*int {
  s := []*int{new(int), new(int), new(int), new(int)}
   //只要h函数调用返回的切片仍在被使用中,它的各个元素就不会回收,包括首尾两个已经丢失的元素。
   //因此这两个已经丢失的元素引用着的两个int值也不会被回收,即使我们再也无法使用这两个int值。
  return s[1:3:3]
}

//为了防止这样的暂时性内存泄露,我们必须重置丢失的元素中的指针。
func h() []*int {
   s := []*int{new(int), new(int), new(int), new(int)}
   s[0], s[len(s)-1] = nil, nil // 重置首尾元素指针
   return s[1:3:3]
}
// 1.3 子字符串导致内存溢出
var s0 string

func f(s1 string) {
   s0 = s1[:50]
   // 目前,s0和s1共享着承载它们的字节序列的同一个内存块。
   // 虽然s1到这里已经不再被使用了,但是s0仍然在使用中,
   // 所以它们共享的内存块将不会被回收。虽然此内存块中
   // 只有50字节被真正使用,而其它字节却无法再被使用。
}

//Go 1.12开始,利用strings.Builder来防止一次不必要的复制
func f(s1 string) {
   var b strings.Builder
   b.Grow(50)
   b.WriteString(s1[:50])
   s0 = b.String()
}
//Go 1.17开始,strings标准库包中引入了一个Clone函数。调用此函数为克隆一个字符串的最佳实现方式。
//2.4 循环中使用defer的问题
for i:=0;i<=100;i++{
   f,_:=os.Open("/etc/hosts")
   //defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销
   //本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。
   defer f.Close()

重点goroutine内存泄露

  1. 生产者goroutine打满阻塞:发送端channel满了,会导致无法GC

  2. 消费者goroutine等待阻塞:接收端等待的channel为空,会导致无法GC

  3. 向 nil channel 发送和接收数据都会导致阻塞。比如我们定义 channel 时忘记初始化

  4. 生产者或消费者异常退出导致阻塞:生产者或消费者异常退出导致channel满了或者空了,会导致无法GC

  5. 忘记关闭channel不会导致GC,只要该channel没有继续被使用,就会被回收;关闭channel一般用于通知其他协程某个任务已经完成了

  6. 解决方案建议

  7. 1. 通过context.WithTimeout做超时处理

       2. 通过select来设置超时处理


以上解释性demo

// 1 没有消费者,只有生产者,生产者的channel会被打满,然后进入阻塞状态
ch := make(chan int)
go func() {
   ch <- 1
   fmt.Println(111)
}()
// 2 没有生产者,只有消费者,消费者的channel一直在等待数据,迟迟没有数据,处于阻塞状态
ch := make(chan int, 1)
go func() {
   <-ch
   fmt.Println(111)
}()
// 4 异常退出
//作为生产者的goroutine:如果数据发送完,应该主动退出当前goroutine并且发送推出信号给消费者goroutine,避免出现`消费者等待阻塞`的情况
//作为消费者的goroutine:一定要在channel没有数据且生产者中的goroutine已经退出的情况下,再退出,避免出现`生产者打满阻塞`的情况
// 6.1 通过context.WithTimeout来预防goroutine不能及时退出的问题
//设置有效时间为1000毫秒
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*1000))
go func() {
   dosomething()//业务逻辑
   // 1000ms以内完成了业务逻辑则取消超时
   defer cancel()
}()

select {
   //1000毫秒后,以上goroutine依然没有完成业务,即可能阻塞了,我们就触发超时退出
   case <-ctx.Done():
  fmt.Println("time out!!!")
  return
}
// 6.2 通过select设置超时处理
done := make(chan struct{}, 1)
go func() {
   dosomething()//业务逻辑
   done <- struct{}{}
}()

select {
   case <-done:
  //通过channel接收业务完成的通知
  fmt.Println("call successfully!!!")
  return
   case <-time.After(time.Duration(1000 * time.Millisecond)):
  //设置超时退出的时间
  fmt.Println("timeout!!!")
  return
}


关于内存溢出的排查以及pprof的使用,请查看下一篇文章。


今天的排版玩儿了两个小时,笑死!!!

关于结构性的内容排版,微信公众号的官方实在是太难使用了,我写内容是在typora上写的,然后复制粘贴到微信公众号的编辑内容里,但是经常会出现代码的排版或内容的结构排版混乱,比如1.1.1,1.1.a这种结构就会错乱掉,事实上我可能还无法舍弃这种结构,因为结构恰恰说明了他们之间的逻辑上的关系。

大家有没有好的排版的工具或者方法推荐,方面我继续用markdow写内容。