golang map内存泄露

在 Go 中使用 map 时,我们需要了解 map 如何增长和收缩的一些重要特征。让我们深入研究一下,以防止可能导致内存泄漏的问题。
首先,要查看此问题的具体示例,让我们设计一个场景,其中我们将使用以下 map:
每个m值都是一个 128 字节的数组。我们将执行以下操作:
1.分配一个空的 map。
2.添加 100 万个元素。
3.删除所有元素,并运行垃圾收集(GC)。
在每一步之后,我们都要打印堆的大小(使用printAlloc实用程序函数)。它向我们展示了此示例的内存行为
package main import ( "fmt" "runtime" ) func randBytes() [128]byte {    return [128]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][128]byte, 0)    printAlloc()    for i := 0; i < n; i++ { //添加100w个元素        m[i] = randBytes()    }    printAlloc()        for i := 0; i < n; i++ { //删除100w个元素        delete(m, i)    }        runtime.GC() //手动触发GC    printAlloc()    runtime.KeepAlive(m) //保持对M的引用,以便不被自动回收 }
运行结果
0 MB
461 MB //添加100w元素以后
293 MB //删除100w元素以后
为什么在向 map 写入了 100w 个 kv 之后,占用内存变成了 461MB?
我们知道,当 val 大小 <= 128B 时,val 其实是直接放在 bucket 里的,按理说,写入 kv 与否,这些 bucket 占用的内存都在那里。换句话说,写入 kv 之后,占用的内存应该还是 293MB,实际上却是 461MB。
这里的原因其实是在写入 100w kv 期间 map 发生了扩容,buckets 进行了搬迁。额外创建的桶也保持不变了
这里有两种解决方案
一、重新创建一个当前map副本,这个的主要缺点是,在复制以后,在下次垃圾收集之前,可能在这段时间内消耗双份的内存
二、另外一种是更改map的类型,改成存储数组指针:map[int]*[128]byte,每一个桶都将为元素保留指针的大小的空间,而不是128字节
在go中的map添加一个元素,然后再删除它,意味在内存中将保留这些桶,目前没有自动化的策略缩小它,所以这导致了高能耗,可以选择重新创建一个map或者是否可以使用指针优化。


在线交流