首页IT科技golang map最大容量(Go map 竟然也会发生内存泄露?)

golang map最大容量(Go map 竟然也会发生内存泄露?)

时间2025-08-04 14:02:55分类IT科技浏览6887
导读:Go 程序运行时,有些场景下会导致进程进入某个“高点”,然后就再也下不来了。...

Go 程序运行时                 ,有些场景下会导致进程进入某个“高点                 ”                          ,然后就再也下不来了                 。

比如         ,多年前曹大写过的一篇文章讲过        ,在做活动时线上涌入的大流量把 goroutine 数抬升了不少                          ,流量恢复之后 goroutine 数也没降下来                  ,导致 GC 的压力升高        ,总体的 CPU 消耗也较平时上升了 2 个点左右                          。

有一个 issue 讨论为什么 allgs(runtime 中存储所有 goroutine 的一个全局 slice) 不收缩                         ,一个好处是:goroutine 复用                  ,让 goroutine 的创建更加得便利,而这也正是 Go 语言的一大优势         。

最近在看《100 mistakes》                         ,书里专门有一节讲 map 的内存泄漏                 。其实这也是另一个在经历大流量后                          ,无法“恢复                          ”的例子:map 占用的内存“只增不减         ”                          。

之前写过的一篇《深度解密 Go 语言之 map》里讲到过 map 的内部数据结构,并且分析过创建                 、遍历                          、删除的过程         。

在 Go runtime 层                 ,map 是一个指向 hmap 结构体的指针                          ,hmap 里有一个字段 B         ,它决定了 map 能存放的元素个数        。

hamp 结构体代码如下:

type hmap struct { count int flags uint8 B uint8 // ... }

若我们想初始化一个长度为 100w 元素的 map                 ,B 是多少呢?

用 B 可以计算 map 的元素个数:loadfactor * 2^B                          ,loadfactor 目前是 6.5         ,当 B=17 时        ,可放 851,968 个元素;当 B=18                          ,可放 1,703,936 个元素                          。因此当我们将 map 的长度初始化为 100w 时                  ,B 的值应是 18                 。

loadfactor 是装载因子        ,用来衡量平均一个 bucket 里有多少个 key        。

如何查看占用的内存数量呢?用 runtime.MemStats:

package main import ( "fmt" "runtime" ) const N = 128 func randBytes() [N]byte { return [N]byte{} } func printAlloc() { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%d MB\n", m.Alloc/1024/1024) } func main() { n := 1_000_000 m := make(map[int][N]byte, 0) printAlloc() for i := 0; i < n; i++ { m[i] = randBytes() } printAlloc() for i := 0; i < n; i++ { delete(m, i) } runtime.GC() printAlloc() runtime.KeepAlive(m) }

如果不加最后的 KeepAlive                         ,m 会被回收掉                          。

当 N = 128 时                  ,运行程序:

$ go run main2.go 0 MB 461 MB 293 MB

可以看到,当删除了所有 kv 后                         ,内存占用依然有 293 MB                          ,这实际上是创建长度为 100w 的 map 所消耗的内存大小                 。当我们创建一个初始长度为 100w 的 map:

package main import ( "fmt" "runtime" ) const N = 128 func printAlloc() { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%d MB\n", m.Alloc/1024/1024) } func main() { n := 1_000_000 m := make(map[int][N]byte, n) printAlloc() runtime.KeepAlive(m) }

运行程序,得到 100w 长度的 map 的消耗的内存为:

$ go run main3.go 293 MB

这时有一个疑惑                 ,为什么在向 map 写入了 100w 个 kv 之后                          ,占用内存变成了 461MB?

我们知道         ,当 val 大小 <= 128B 时                 ,val 其实是直接放在 bucket 里的                          ,按理说         ,写入 kv 与否        ,这些 bucket 占用的内存都在那里。换句话说                          ,写入 kv 之后                  ,占用的内存应该还是 293MB        ,实际上却是 461MB                          。

这里的原因其实是在写入 100w kv 期间 map 发生了扩容                         ,buckets 进行了搬迁                          。我们可以用 hack 的方式打印出 B 值:

func main() { //... var B uint8 for i := 0; i < n; i++ { curB := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(*(**int)(unsafe.Pointer(&m)))) + 9)) if B != curB { fmt.Println(curB) B = curB } m[i] = randBytes() } //... runtime.KeepAlive(m) }

运行程序                  ,B 值从 1 一直变到 18。搬迁的过程可以参考前面提到的那篇 map 文章,这里不再赘述                 。

而如果我们初始化的时候直接将 map 的长度指定为 100w                         ,那内存变化情况为:

293 MB 293 MB 293 MB

当 val 小于 128B 时                          ,初始化 map 后内存占用量一直不变                          。原因是 put 操作只是在 bucket 里原地写入 val,而 delete 操作则是将 val 清零                 ,bucket 本身还在         。因此                          ,内存占用大小不变                 。

而当 val 大小超过 128B 后         ,bucket 不会直接放 val                 ,转而变成一个指针                          。我们将 N 设为 129                          ,运行程序:

0 MB 197 MB 38 MB

虽然 map 的 bucket 占用内存量依然存在         ,但 val 改成指针存储后内存占用量大大降低         。且 val 被删掉后        ,内存占用量确实降低了        。

总之                          ,map 的 buckets 数只会增                  ,不会降                          。所以在流量冲击后        ,map 的 buckets 数增长到一定值                         ,之后即使把元素都删了也无济于事                 。内存占用还是在                  ,因为 buckets 占用的内存不会少        。

对于 map 内存泄漏的解法:

重启; 将 val 类型改成指针; 定期地将 map 里的元素全量拷贝到另一个 map 里                          。

好在一般有大流量冲击的互联网业务大都是 toC 场景,上线频率非常高                 。有的公司能一天上线好几次                         ,在问题暴露之前就已经重启恢复了                          ,问题不大。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
quicktimeplayer倍速播放没有声音(quicktimeplayer.exe – quicktimeplayer是什么进程 有什么用) 网站流量提升(提升网站流量的方法有哪些?)