Go/Golang map内存问题解决方案

摘要: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 万个元素后
*/

与原来直接存放值作对比,内存使用减少很多,但是这不能改变桶数不变的事实:

直接存放值存放指针指向值
初始化map105105
增加 100 万个元素后472503186581
删除 100 万个元素后30044039283

note:如果一个键或值超过 128 字节,Go 不会将其直接存储在 map bucket 中。相反,Go 存储一个指针来引用键或值。

感谢阅读,欢迎指正,觉得有用的话就点个赞吧😘