一些八股文
内存泄漏
内存泄漏指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。
Go 虽然有 GC 来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着 Go 程序不再有内存泄漏问题。
10 次内存泄漏,有 9 次是 goroutine 泄漏。
Goroutine 泄漏
Goroutine 泄漏是最常见最常见的内存泄漏了
如果你启动了 1 个 goroutine,但并没有符合预期的退出,直到程序结束,此 goroutine 才退出,这种情况就是 goroutine 泄漏。
如果 Goroutine 在执行时被阻塞而无法退出,就会导致 Goroutine 的内存泄漏,一个 Goroutine 的最低栈大小为 2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。
互斥锁未释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 协程拿到锁未释放,其他协程获取锁会阻塞
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)
// mutex.UnLock()
}()
}
time.Sleep(10 * time.Second)
}
|
死锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 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() // block
fmt.Println("g1 get m2")
}()
// g2得到锁2去获取锁1
go func() {
m2.Lock()
fmt.Println("g2 get m2")
time.Sleep(1 * time.Second)
m1.Lock() // block
fmt.Println("g2 get m1")
}()
// 其余协程获取锁都会失败
go func() {
m1.Lock() // block
fmt.Println("g3 get m1")
}()
time.Sleep(10 * time.Second)
}
|
空 channel
声明未初始化的 channel 读写都会阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| func channelTest() {
var c chan int // 未初始化
// 向channel中写数据
go func() {
c <- 1 // block
fmt.Println("g1 send succeed")
time.Sleep(1 * time.Second)
}()
// 从channel中读数据
go func() {
<-c // block
fmt.Println("g2 receive succeed")
time.Sleep(1 * time.Second)
}()
time.Sleep(10 * time.Second)
}
|
channel 写多于读
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func channelTest() {
var c = make(chan int)
// 10个协程向channel中写数据
for i := 0; i < 10; i++ {
go func() {
c <- 1
fmt.Println("g1 send succeed")
time.Sleep(1 * time.Second)
}()
}
// 1个协程丛channel中读数据
go func() {
<-c
fmt.Println("g2 receive succeed")
time.Sleep(1 * time.Second)
}()
// 会有写的9个协程阻塞得不到释放
time.Sleep(10 * time.Second)
}
|
channel 读多于写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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)
}
|
其他泄漏
获取长字符串中的一段导致长字符串未释放
1
2
3
4
5
6
7
8
9
10
11
12
| var s0 string // a package-level variable
// A demo purpose function.
func f(s1 string) {
s0 = s1[:50]
// Now, s0 shares the same underlying memory block
// with s1. Although s1 is not alive now, but s0
// is still alive, so the memory block they share
// couldn't be collected, though there are only 50
// bytes used in the block and all other bytes in
// the block become unavailable.
}
|
解决方案
1
2
3
| func f(s1 string) {
s0 = (" " + s1[:50])[1:]
}
|
获取长 slice 中的一段导致长 slice 未释放
异曲同工
1
2
3
4
5
6
7
8
9
10
11
| var s0 []int
func g(s1 []int) {
// Assume the length of s1 is much larger than 30.
s0 = s1[len(s1)-30:]
}
func demo() {
s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
f(s)
}
|
解决方案:拷贝
1
2
3
4
5
6
7
| func g(s1 []int) {
s0 = make([]int, 30)
copy(s0, s1[len(s1)-30:])
// Now, the memory block hosting the elements
// of s1 can be collected if no other values
// are referencing the memory block.
}
|
长 slice 新建 slice 导致泄漏
1
2
3
4
5
6
| func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...
return s[1:3:3]
}
|
解决方案:
1
2
3
4
5
6
7
8
| func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
// do something with s ...
// Reset pointer values.
s[0], s[len(s)-1] = nil, nil
return s[1:3:3]
}
|
定时器泄漏
time.Ticker
每隔指定时间就会向通道内写数据。作为循环触发器,必须调用 stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| func tickerTest() {
// 定义一个ticker,每隔500毫秒触发
ticker := time.NewTicker(time.Second * 1)
// Ticker触发
go func() {
for t := range ticker.C {
fmt.Println("ticker被触发", t)
}
}()
time.Sleep(time.Second * 10)
// 停止ticker
ticker.Stop()
}
|