Go 语言中的map和内存泄漏

Map在内存中总是会增长;它不会收缩 。因此,如果map导致了一些内存问题,你可以尝试不同的选项 , 比如强制 Go 重新创建map或使用指针 。

Go 语言中的map和内存泄漏

文章插图
在 Go 中使用map时,我们需要了解map增长和收缩的一些重要特性 。让我们深入探讨这一点,以防止可能导致内存泄漏的问题 。
首先,为了查看这个问题的一个具体例子,让我们设计一个场景,在这个场景中我们将使用以下map:
m := make(map[int][128]byte)每个 m 的值都是一个包含 128 字节的数组 。我们将执行以下操作:
  • 分配一个空的map 。
  • 添加 100 万个元素 。
  • 删除所有元素,并运行垃圾回收(GC) 。
在每个步骤之后 , 我们希望打印堆的大?。ㄊ褂靡桓?nbsp;printAlloc 实用函数) 。这将展示这个示例在内存方面的行为方式:
func mAIn() {n := 1_000_000m := make(map[int][128]byte)printAlloc()for i := 0; i < n; i++ { // Adds 1 million elementsm[i] = [128]byte{}}printAlloc()for i := 0; i < n; i++ { // Deletes 1 million elementsdelete(m, i)}runtime.GC() // Triggers a manual GCprintAlloc()runtime.KeepAlive(m) // Keeps a reference to m so that the map isn’t collected}func printAlloc() {var m runtime.MemStatsruntime.ReadMemStats(&m)fmt.Printf("%d KBn", m.Alloc/1024)}我们分配一个空的map,添加 100 万个元素,删除 100 万个元素 , 然后运行垃圾回收 。我们还确保使用 runtime.KeepAlive 保持对map的引用,以防止map被收集 。让我们运行这个示例:
0 MB<-- After m is allocated461 MB <-- After we add 1 million elements293 MB <-- After we remove 1 million elements我们观察到了什么?起初,堆大小很小 。然后 , 在将 100 万个元素添加到map后,它显著增长了 。但是 , 如果我们期望在删除所有元素后堆大小会减?。?獠⒉皇?Go 中map的工作方式 。最后,尽管 GC 已经收集了所有元素,但堆大小仍然是 293 MB 。因此 , 内存缩小了 , 但并非我们可能预期的方式 。这其中的原理是什么?我们需要深入了解一下 Go 中map的工作原理 。
map提供了一个无序的键值对集合 , 其中所有的键都是唯一的 。在 Go 中,map基于哈希表数据结构:一个数组,其中每个元素都是指向键值对存储桶的指针,如图1所示 。
Go 语言中的map和内存泄漏

文章插图
图1 — 哈希表示例,重点关注存储桶 0 。
每个存储桶都是一个固定大小的数组 , 包含八个元素 。如果要将元素插入已经满了的存储桶(即存储桶溢出),Go 会创建另一个包含八个元素的存储桶,并将前一个存储桶链接到它上 。图2显示了一个例子:
Go 语言中的map和内存泄漏

文章插图
图2 — 如果存储桶溢出,Go 会分配一个新的存储桶 , 并将前一个存储桶链接到它上 。
在底层,Go 中的map是指向 runtime.hmap 结构体的指针 。该结构体包含多个字段,其中包括一个 B 字段,表示map中存储桶的数量:
type hmap struct {B uint8 // log_2 of # of buckets// (can hold up to loadFactor * 2^B items)// ...}在添加了100万个元素之后,B 的值等于18,这意味着有 2¹? = 262,144 个存储桶 。当我们删除了100万个元素后,B 的值是多少呢?仍然是18 。因此 , map仍然包含相同数量的存储桶 。
原因在于map中存储桶的数量是不可缩减的 。因此,从map中删除元素不会影响现有存储桶的数量;它只是将存储桶中的槽清零 。map只能增长并拥有更多的存储桶;它永远不会缩小 。
在先前的示例中,我们从461 MB减少到了293 MB , 因为元素被收集,但运行垃圾回收并没有影响map本身 。即使额外存储桶的数量(因为溢出而创建的存储桶)也保持不变 。
【Go 语言中的map和内存泄漏】让我们退一步,讨论map无法缩小的情况何时可能成为问题 。想象一下使用 map[int][128]byte 来构建缓存 。这个map以每个客户ID(int)为键,保存一个长度为128字节的序列 。现在,假设我们想保存最近的1000位客户 。map的大小将保持不变,所以我们不必担心map无法缩小的问题 。
但是,假设我们想要存储一小时的数据 。同时,我们的公司决定在黑色星期五进行大促销:在一个小时内,我们可能会有数百万的客户连接到我们的系统 。但是在黑色星期五之后的几天,我们的map将包含与高峰期相同数量的存储桶 。这就解释了为什么在这种情况下我们可能会遇到内存消耗高却不会显著减少的情况 。


推荐阅读