ss网站代码,xp系统做网站服务器,咸阳免费做网站公司,天元建设集团有限公司楼盘map在内存中总是会增长#xff1b;它不会收缩。因此#xff0c;如果map导致了一些内存问题#xff0c;你可以尝试不同的选项#xff0c;比如强制 Go 重新创建map或使用指针。 在 Go 中使用map时#xff0c;我们需要了解map增长和收缩的一些重要特性。让我们深入探讨这一点…map在内存中总是会增长它不会收缩。因此如果map导致了一些内存问题你可以尝试不同的选项比如强制 Go 重新创建map或使用指针。 在 Go 中使用map时我们需要了解map增长和收缩的一些重要特性。让我们深入探讨这一点以防止可能导致内存泄漏的问题。
首先为了查看这个问题的一个具体例子让我们设计一个场景在这个场景中我们将使用以下map
m : make(map[int][128]byte)每个 m 的值都是一个包含 128 字节的数组。我们将执行以下操作
分配一个空的map。添加 100 万个元素。删除所有元素并运行垃圾回收GC。
在每个步骤之后我们希望打印堆的大小使用一个 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 KB\n, m.Alloc/1024)
}我们分配一个空的map添加 100 万个元素删除 100 万个元素然后运行垃圾回收。我们还确保使用 runtime.KeepAlive 保持对map的引用以防止map被收集。让我们运行这个示例
0 MB -- After m is allocated
461 MB -- After we add 1 million elements
293 MB -- After we remove 1 million elements我们观察到了什么起初堆大小很小。然后在将 100 万个元素添加到map后它显著增长了。但是如果我们期望在删除所有元素后堆大小会减小这并不是 Go 中map的工作方式。最后尽管 GC 已经收集了所有元素但堆大小仍然是 293 MB。因此内存缩小了但并非我们可能预期的方式。这其中的原理是什么我们需要深入了解一下 Go 中map的工作原理。
map提供了一个无序的键值对集合其中所有的键都是唯一的。在 Go 中map基于哈希表数据结构一个数组其中每个元素都是指向键值对存储桶的指针如图1所示。 图1 — 哈希表示例重点关注存储桶 0。
每个存储桶都是一个固定大小的数组包含八个元素。如果要将元素插入已经满了的存储桶即存储桶溢出Go 会创建另一个包含八个元素的存储桶并将前一个存储桶链接到它上。图2显示了一个例子 图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本身。即使额外存储桶的数量因为溢出而创建的存储桶也保持不变。
让我们退一步讨论map无法缩小的情况何时可能成为问题。想象一下使用 map[int][128]byte 来构建缓存。这个map以每个客户IDint为键保存一个长度为128字节的序列。现在假设我们想保存最近的1000位客户。map的大小将保持不变所以我们不必担心map无法缩小的问题。
但是假设我们想要存储一小时的数据。同时我们的公司决定在黑色星期五进行大促销在一个小时内我们可能会有数百万的客户连接到我们的系统。但是在黑色星期五之后的几天我们的map将包含与高峰期相同数量的存储桶。这就解释了为什么在这种情况下我们可能会遇到内存消耗高却不会显著减少的情况。
如果我们不想手动重启服务来清理map消耗的内存量有哪些解决方案一种解决方案可以是定期重新创建当前map的副本。例如每小时我们可以构建一个新map复制所有元素并释放先前的map。这种选择的主要缺点是在复制后直到下一次垃圾回收之前我们可能会在短时间内消耗两倍于当前内存。
另一种解决方案是将map类型更改为存储数组指针map[int]*[128]byte。这并没有解决我们会有大量存储桶的问题然而每个存储桶条目将为值保留指针的大小而不是128字节64位系统上为8字节32位系统上为4字节。
回到原始场景让我们比较每种map类型在每个步骤后的内存消耗。以下表格显示了比较。
Stepmap[int][128]bytemap[int]*[128]byte分配一个空的 map0 MB0 MB添加100万个元素461 MB182 MB删除所有元素并运行GC293 MB38 MB
正如我们所看到的在删除所有元素后使用 map[int]*[128]byte 类型所需的内存量明显较少。此外在这种情况下由于一些优化措施以减少内存消耗高峰时期所需的内存量也较少显著。
注意如果键或值超过128字节Go 将不会直接将其存储在map存储桶中。相反Go 将存储用于引用键或值的指针。
结论
正如我们所见向map添加 n 个元素然后删除所有元素意味着在内存中保持相同数量的存储桶。因此我们必须记住由于 Go map只能增长因此其内存消耗也会随之增加。它没有自动化的策略来缩小。如果这导致内存消耗过高我们可以尝试不同的选项比如强制 Go 重新创建map或使用指针来检查是否可以进行优化。