go map gc的测试代码


**本文章由github copilot协助生成**
遇到一个离职同事写的代码,如下:

```go
package mapGC
func mapGc() {
lock := sync.Mutex{}
go func() {
for {
time.Sleep(12 * time.Hour)
tmp := make(map[string]interface{})
lock.Lock()
tmp = GlobalMap
GlobalMap = nil
GlobaMap = tmp
lock.Unlock()
}
}()
}
```
我们就很怀疑, go不是有自动GC吗? 为什么还要再写个手动gc?
于是就开始研究, 为什么要这么写, 有什么好处, 有什么坏处, 有没有更好的写法?
我们知道, 如果map里面的kv被删除后, 如果kv没有被外界引用, 在一定情况下go会进行自动gc,所以kv所占用的内存不需要用上面的形式进行释放.

map内部实现是基于hash, 参考文章:https://blog.csdn.net/cyq6239075/article/details/106047992?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4-106047992-blog-116662891.235%5Ev36%5Epc_relevant_default_base3&spm=1001.2101.3001.4242.3&utm_relevant_index=7
在看了上面的文章之后, 怀疑这段代码想删除的其实是map中多余的bucket. 但是这个bucket是不是真的会占用内存呢?其实是会的.
map的底层实现是一个数组, 数组的每个元素是一个bucket, bucket中存储了key, value, 以及一个指向下一个bucket的指针.
如果map中的key很多, 但是value很少, 那么就会造成很多bucket中的value都是nil, 这样就会造成内存的浪费.
所以这段代码的目的是, 每隔12小时, 将map中的bucket中的value为nil的bucket删除掉, 从而减少内存的浪费.

但是这段代码有一个问题, 就是在删除bucket的时候, 会造成map的不可用, 因为在删除bucket的时候, 会对map进行加锁, 这样在删除的时候, 其他的goroutine就无法对map进行读写了.
所以这段代码的目的是好的, 但是实现的方式不好.

首先这段代码在我们的业务中是没有太大必要的, 因为我们业务属于周期性比较强的场景, 每晚都会有大量的数据写入, 所以不会出现key很多, value很少的情况.
其次有人可能会有疑问, 如果v比较大, 那么map size比较大的情况下, 也会占用比较大的内存, 其实不会的, 因为go在实现map的时候,如果sizeof(v)很大, 那么go会将v存储在heap中, 然后在map中存储的是v的指针, 所以map的size不会很大.
最后附录上我写的一个测试代码, 用来测试map的内存占用情况.

```go
package main

import (
"log"
"math/rand"
"runtime"
"unsafe"
)

type T struct {
a int
b int
c int
d int
str string
balance [500000]float32
}

var intMap map[int]T

func getInfo(m map[string]string) (int, int) {
point := (**T)(unsafe.Pointer(&m))
value := *point
return value.a, int(value.b)
}

func main() {
printMemStats("main") // main:分配的内存 = 106KB, GC的次数 = 0
// 添加1w个map值
intMap = make(map[int]T, 100000)
runtime.GC()
printMemStats("初始化") // 分配的内存 = 7997KB, GC的次数 = 2
for i := 0; i < 100000; i++ {
t := T{}
intMap[rand.Intn(2147483647)] = t
}

// 手动进行gc操作
runtime.GC()
// 再次查看数据
printMemStats("增加map数据后") // 分配的内存 = 8789KB, GC的次数 = 3
for k := range intMap {
delete(intMap, k)
}
log.Print("map.len:", len(intMap))
printMemStats("1.删除map数据后") //分配的内存 = 8790KB, GC的次数 = 3
// 再次进行手动GC回收
runtime.GC()
printMemStats("2.删除map数据后并且gc后.") //分配的内存 = 8008KB, GC的次数 = 4
for i := 0; i < 10000; i++ {
t := T{}
intMap[rand.Intn(2147483647)] = t
}
for k := range intMap {
delete(intMap, k)
}
runtime.GC()
printMemStats("3.删除map数据后并且gc后.") //
// for cnt := 0; cnt < 10000; cnt++ {
// // log.Println("删除前数组长度:", len(intMap))
// // for i := 0; i < 100000; i++ {
// // delete(intMap, i)
// // }
// for k := range intMap {
// delete(intMap, k)
// }
// // log.Println("删除后数组长度:", len(intMap))

// // 再次进行手动GC回收
// // runtime.GC()
// // printMemStats("删除map数据后")

// // for i := 0; i < 100000; i++ {
// // intMap[i] = i
// // }
// for i := 0; i < 100000; i++ {
// t := T{
// a: i,
// }
// intMap[rand.Intn(2147483647)] = t
// }
// // if cnt == 9999 {
// // printMemStats("---------------重新添加·map数据后")
// // }
// }
// printMemStats("---------------重新添加·map数据后")
// runtime.GC()
// printMemStats("----手动gc后")

// // 设置为nil进行回收
// intMap = nil
// runtime.GC()
// printMemStats("设置为nil后")
}

func printMemStats(mag string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("%v:分配的内存 = %vKB, GC的次数 = %v\n", mag, m.Alloc/1024, m.NumGC)
}
```



posted @ 2023-06-05 16:19  yushimeng  阅读(34)  评论(0编辑  收藏  举报