No.7 一篇文章讲清楚golang内存泄漏

简介: No.7 一篇文章讲清楚golang内存泄漏

640.png

一篇文章讲清楚golang内存泄漏,Go必知必会,6分钟

  1. 什么是"内存泄漏"?

内存泄漏并不是指物理上的内存消失,而是在写程序的过程中,由于程序的设计不合理导致对之前使用的内存失去控制,无法再利用这块内存区域;短期内的内存泄漏可能看不出什么影响,但是当时间长了之后,日积月累,浪费的内存越来越多,导致可用的内存空间减少,轻则影响程序性能,严重可导致正在运行的程序突然崩溃。

一般一个进程结束之后,内存会自动回收,同时也会自动回收那些被泄露的内存,当进程重新启动后,这些内存又可以重新被分配使用。但是正常情况下企业的程序是不会经常重启的,所以最好的办法就是从源头上解决内存泄漏的问题。

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导致的内存泄漏

  1. 申请过多的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/



相关文章
|
6月前
|
存储 编译器 Go
Golang底层原理剖析之内存对齐
Golang底层原理剖析之内存对齐
54 0
|
存储 算法 编译器
Golang 语言的内存管理
Golang 语言的内存管理
49 0
|
安全 编译器 Go
Golang 语言的内存模型
Golang 语言的内存模型
58 0
一篇文章让你看懂C语言字符函数和内存函数(上)
一篇文章让你看懂C语言字符函数和内存函数(上)
|
5月前
|
NoSQL Java Redis
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
Redis系列学习文章分享---第十八篇(Redis原理篇--网络模型,通讯协议,内存回收)
83 0
|
3月前
|
NoSQL Java 测试技术
Golang内存分析工具gctrace和pprof实战
文章详细介绍了Golang的两个内存分析工具gctrace和pprof的使用方法,通过实例分析展示了如何通过gctrace跟踪GC的不同阶段耗时与内存量对比,以及如何使用pprof进行内存分析和调优。
83 0
Golang内存分析工具gctrace和pprof实战
|
3月前
|
Prometheus Cloud Native Java
解决golang 的内存碎片问题
解决golang 的内存碎片问题
72 3
|
3月前
|
Kubernetes 网络协议 测试技术
记一次golang内存泄露
记一次golang内存泄露
45 4
|
6月前
|
监控 Java 编译器
Java的内存模型与并发控制技术性文章
Java的内存模型与并发控制技术性文章
47 2
|
5月前
|
Linux 芯片
一篇文章讲明白Linux内核态和用户态共享内存方式通信
一篇文章讲明白Linux内核态和用户态共享内存方式通信
67 0