摘要:map 总是可以在内存中增长;它从不收缩(因为map只创建桶,删除桶元素的时候不会释放桶的内存,导致map内存不断增长)。因此,如果它导致一些内存问题,可以尝试这三种方法解决,重启服务器,定期创建 map副本或将value存放指向值的指针而不直接存值。
首先提供查看内存函数utils.PrintAlloc()的实现(utils包自己定义):
func PrintAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024)
}
看一个例子,map确实自己不会删除桶
func main() {
n := 1_000_000
m := make(map[int][128]byte)
utils.PrintAlloc()
for i := 0; i < n; i++ {
m[i] = [128]byte{}
}
utils.PrintAlloc()
for i := 0; i < n; i++ {
delete(m, i)
}
runtime.GC() //triggers a manual gc
utils.PrintAlloc()
runtime.KeepAlive(m) //keep a reference to m so that the map isn't collected
}
/*
result:
105 MB <-- m 被分配后 //为什么不是0因为调用printAlloc()函数造成的
472503 MB <-- 我们增加 100 万个元素后
300440 MB <-- 我们删除 100 万个元素后
*/
解决方法一:重启服务器
解决方法二:创建副本map
// 创建副本清理map消耗的内存
func main() {
n := 1_000_000
m := make(map[int][128]byte)
utils.PrintAlloc()
for i := 0; i < n; i++ {
m[i] = [128]byte{}
}
utils.PrintAlloc()
for i := 0; i < n; i++ {
delete(m, i)
}
for i := 0; i < n/2; i++ { //加5000个元素
m[i] = [128]byte{}
}
utils.PrintAlloc()
k := m //创建副本
runtime.GC() //triggers a manual gc
runtime.KeepAlive(k) //keep a reference to m so that the map isn't collected
utils.PrintAlloc()
}
/*
result:
105 MB
472517 MB <----加一万elements之后
472461 MB <----源map一万个bucket,5000个element
300440 MB <----副本map五千个bucket,5000个element
*/
可以看见源map一万个bucket,5000个element占用472461MB,副本map五千个bucket,5000个element占用300440,通过创建副本map确实减少了bucket的数量,但是这也存在问题,就是当副本创建之后gc回收之前将占用大量内存,因为此时存在两个map。
解决方法三:value存放指向值的指针
package main
import (
"map-memory-leak/utils"
"runtime"
)
// 用指针引用value,减少内存使用,并不能解决我们存在大量bucket的事实
func main() {
n := 1_000_000
k := make(map[int]*[128]byte)
utils.PrintAlloc()
for i := 0; i < n; i++ {
k[i] = &[128]byte{}
}
utils.PrintAlloc()
for i := 0; i < n; i++ {
delete(k, i)
}
runtime.GC() //triggers a manual gc
utils.PrintAlloc()
runtime.KeepAlive(k) //keep a reference to m so that the map isn't collected
}
/*
result:
105 MB <-- m 被分配后 //为什么不是0因为调用printAlloc()函数造成的
186581 MB <-- 我们增加 100 万个元素后
39283 MB <-- 我们删除 100 万个元素后
*/
与原来直接存放值作对比,内存使用减少很多,但是这不能改变桶数不变的事实:
直接存放值 | 存放指针指向值 | |
---|---|---|
初始化map | 105 | 105 |
增加 100 万个元素后 | 472503 | 186581 |
删除 100 万个元素后 | 300440 | 39283 |
note:如果一个键或值超过 128 字节,Go 不会将其直接存储在 map bucket 中。相反,Go 存储一个指针来引用键或值。
感谢阅读,欢迎指正,觉得有用的话就点个赞吧😘