上文说到面试官考察了超超数组和切片的区别,超超答道了切片使用不当会导致内存泄漏的场景。那么除了切片的错误使用会导致内存泄漏,有没有其他场景会导致内存泄漏呢?来看看超超是怎么理解的吧~

数组的错误使用

面试官:在开发中数组使用不当会导致内存泄漏吗?

超超:由于数组是Golang的基本数据类型,每个数组占用不同的内存空间,生命周期互不干扰,很难出现内存泄漏的情况。但是数组作为形参传输时,遵循的是值拷贝,如果函数被多次调用且数组过大时,则会导致内存使用激增。

//统计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
}

例如上面的函数中,每次调用countTarget函数传参时都需要新建一个大小为100万的int数组,大约为8MB内存,如果在短时间内调用100次就需要约800MB的内存空间了。(未达到GC时间或者GC阀值是不会触发GC的)如果是在高并发场景下每个协程都同时调用该函数,内存占用量是非常恐怖的。因此对于大数组放在形参场景下,通常使用切片或者指针进行传递,避免短时间的内存使用激增。

Goroutine泄漏

面试官:对于内存泄漏,还会在哪些场景下会触发?

超超:实际开发中更多的还是Goroutine引起的内存泄漏,因为Goroutine的创建非常简单,通过关键字go即可创建,由于开发的进度大部分程序猿只会关心代码的功能是否实现,很少会关心Goroutine何时退出。如果Goroutine在执行时被阻塞而无法退出,就会导致Goroutine的内存泄漏,一个Goroutine的最低栈大小为2KB,在高并发的场景下,对内存的消耗也是非常恐怖的!

互斥锁未释放

 1//协程拿到锁未释放,其他协程获取锁会阻塞
 2func mutexTest() {
 3    mutex := sync.Mutex{}
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            mutex.Lock()
 7            fmt.Printf("%d goroutine get mutex", i)
 8      //模拟实际开发中的操作耗时
 9            time.Sleep(100 * time.Millisecond)
10        }()
11    }
12    time.Sleep(10 * time.Second)
13}

死锁

 1func mutexTest() {
 2    m1, m2 := sync.Mutex{}, sync.RWMutex{}
 3  //g1得到锁1去获取锁2
 4    go func() {
 5        m1.Lock()
 6        fmt.Println("g1 get m1")
 7        time.Sleep(1 * time.Second)
 8        m2.Lock()
 9        fmt.Println("g1 get m2")
10    }()
11    //g2得到锁2去获取锁1
12    go func() {
13        m2.Lock()
14        fmt.Println("g2 get m2")
15        time.Sleep(1 * time.Second)
16        m1.Lock()
17        fmt.Println("g2 get m1")
18    }()
19  //其余协程获取锁都会失败
20    go func() {
21        m1.Lock()
22        fmt.Println("g3 get m1")
23    }()
24    time.Sleep(10 * time.Second)
25}

空channel

 1func channelTest() {
 2  //声明未初始化的channel读写都会阻塞
 3    var c chan int
 4  //向channel中写数据
 5    go func() {
 6        c <- 1
 7        fmt.Println("g1 send succeed")
 8        time.Sleep(1 * time.Second)
 9    }()
10  //从channel中读数据
11    go func() {
12        <-c
13        fmt.Println("g2 receive succeed")
14        time.Sleep(1 * time.Second)
15    }()
16    time.Sleep(10 * time.Second)
17}

能出不能进

 1func channelTest() {
 2    var c = make(chan int)
 3  //10个协程向channel中写数据
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            c <- 1
 7            fmt.Println("g1 send succeed")
 8            time.Sleep(1 * time.Second)
 9        }()
10    }
11  //1个协程丛channel中读数据
12    go func() {
13        <-c
14        fmt.Println("g2 receive succeed")
15        time.Sleep(1 * time.Second)
16    }()
17  //会有写的9个协程阻塞得不到释放
18    time.Sleep(10 * time.Second)
19}

能进不能出

 1func channelTest() {
 2    var c = make(chan int)
 3  //10个协程向channel中读数据
 4    for i := 0; i < 10; i++ {
 5        go func() {
 6            <- c
 7            fmt.Println("g1 receive succeed")
 8            time.Sleep(1 * time.Second)
 9        }()
10    }
11  //1个协程丛channel写读数据
12    go func() {
13        c <- 1
14        fmt.Println("g2 send succeed")
15        time.Sleep(1 * time.Second)
16    }()
17  //会有读的9个协程阻塞得不到释放
18    time.Sleep(10 * time.Second)
19}

定时器的使用

面试官:你知道time.Ticker吗?
超超:知道的,time.Ticker是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用stop方法才会停止,从而被GC掉,否则会一直占用内存空间。

 1func tickerTest() {
 2    //定义一个ticker,每隔500毫秒触发
 3    ticker := time.NewTicker(time.Second * 1)
 4    //Ticker触发
 5    go func() {
 6        for t := range ticker.C {
 7            fmt.Println("ticker被触发", t)
 8        }
 9    }()
10
11    time.Sleep(time.Second * 10)
12    //停止ticker
13    ticker.Stop()
14}

面试官:在开发中你知道哪些工具可以来排查内存泄漏吗?

超超:了解的….



对于Golang的内存泄漏场景欢迎小伙伴在下方留言讨论呀!同样欢迎添加我的微信,进读者群和超超一起讨论开发中的难题!