Go map 竟然也会发生内存泄露?

打印 上一主题 下一主题

主题 1024|帖子 1024|积分 3072

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
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 结构体代码如下:
  1. type hmap struct {
  2.         count     int
  3.         flags     uint8
  4.         B         uint8
  5.        
  6.         // ...
  7. }
复制代码
若我们想初始化一个长度为 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:
  1. package main
  2. import (
  3.         "fmt"
  4.         "runtime"
  5. )
  6. const N = 128
  7. func randBytes() [N]byte {
  8.         return [N]byte{}
  9. }
  10. func printAlloc() {
  11.         var m runtime.MemStats
  12.         runtime.ReadMemStats(&m)
  13.         fmt.Printf("%d MB\n", m.Alloc/1024/1024)
  14. }
  15. func main() {
  16.         n := 1_000_000
  17.         m := make(map[int][N]byte, 0)
  18.         printAlloc()
  19.         for i := 0; i < n; i++ {
  20.                 m[i] = randBytes()
  21.         }
  22.         printAlloc()
  23.        
  24.         for i := 0; i < n; i++ {
  25.                 delete(m, i)
  26.         }
  27.        
  28.         runtime.GC()
  29.         printAlloc()
  30.         runtime.KeepAlive(m)
  31. }
复制代码
如果不加最后的 KeepAlive,m 会被回收掉。
当 N = 128 时,运行程序:
  1. $ go run main2.go
  2. 0 MB
  3. 461 MB
  4. 293 MB
复制代码
可以看到,当删除了所有 kv 后,内存占用依然有 293 MB,这实际上是创建长度为 100w 的 map 所消耗的内存大小。当我们创建一个初始长度为 100w 的 map:
  1. package main
  2. import (
  3.         "fmt"
  4.         "runtime"
  5. )
  6. const N = 128
  7. func printAlloc() {
  8.         var m runtime.MemStats
  9.         runtime.ReadMemStats(&m)
  10.         fmt.Printf("%d MB\n", m.Alloc/1024/1024)
  11. }
  12. func main() {
  13.         n := 1_000_000
  14.         m := make(map[int][N]byte, n)
  15.         printAlloc()
  16.         runtime.KeepAlive(m)
  17. }
复制代码
运行程序,得到 100w 长度的 map 的消耗的内存为:
  1. $ go run main3.go
  2. 293 MB
复制代码
这时有一个疑惑,为什么在向 map 写入了 100w 个 kv 之后,占用内存变成了 461MB?

我们知道,当 val 大小
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表