Go map 删除键后不释放桶内存,仅标记 tophash 为 emptyOne;真正释放内存需置 nil 或重建 map。

删除 key 后,Go map 的哈希桶不会复用,也不会收缩,只是把槽位标记为空 —— 这不是 bug,是设计使然。
delete 操作只改 tophash,不移动也不回收 bucket
调用 delete(m, key) 时,Go 做的只是定位到对应 bucket 和槽位,把该位置的 tophash 设为 emptyOne(值为 1),同时清空键和值内存。它完全不改变底层 buckets 数组长度、不降低 hmap.B、不释放任何 bucket 内存。
常见错误现象:len(m) == 0 但 runtime.ReadMemStats().Alloc 几乎不变;用 pprof 看 heap,bucket 占用依旧高。
- 所有主桶(
2^B个)全部保留,哪怕一个元素都没了 - 溢出桶(overflow buckets)仅在数量 ≥ 32768 或 ≥ 主桶数(若
2^B ≤ 32768)时才批量清理 - GC 只能回收键值对本身堆内存,无法回收已分配的 bucket 内存
tophash 数组里的 0–4 是状态位,不是哈希值
每个 bucket 的 tophash [8]uint8 不是单纯存哈希高位,而是复用同一块内存做状态标记:
-
emptyRest = 0:当前空,且后续更高索引或溢出桶里也无数据 -
emptyOne = 1:仅当前槽位为空(delete后设的就是这个) -
evacuatedX = 2/evacuatedY = 3:表示该键值对已迁移到新表的前/后半区 -
evacuatedEmpty = 4:槽位为空,且整个 bucket 已搬迁完毕 - 真正哈希高位从
minTopHash = 5开始,所以 0–4 是安全预留位
这意味着:你看到某个 tophash[i] == 1,它代表“这里删过”,不是“哈希高位碰巧是 1”——这是 Go 哈希查找能跳过空槽的关键优化。
想真正释放内存或恢复性能,必须重建 map
如果你刚删掉 99% 的数据,且后续写入量小、键分布稳定,继续用原 m 会因残留溢出桶 + 稀疏主桶导致哈希冲突上升,插入/查找变慢 10%~30%。
- 别用
clear(m):它等价于循环delete,不释放底层内存 - 正确做法:
m = make(map[K]V, estimatedCap),其中estimatedCap是预估的后续峰值大小 - 长生命周期 map(如全局变量、struct 字段)删光后应立刻
m = nil,否则 GC 无法判定其可回收 - nil 后再写入会 panic,确保无后续访问;若需复用,必须重建
make
sameSizeGrow 不是缩容,是“整理碎片”
当触发 grow(比如写入时发现太多溢出桶),Go 可能走 sameSizeGrow 路径:新建一组相同数量的 bucket,把旧桶 + 溢出桶里的有效元素重新 hash 迁移过去。
这过程不减少内存占用,目的只是把散落的数据收拢,消除冗余溢出链。迁移完旧 bucket 才被 GC 回收,但新 bucket 占用和原来一样多。
真正决定内存能否释放的,从来不是 delete 调用多少次,而是“还有没有变量能访问到这个 map 对象”。delete 只是擦除内容,nil 才是切断引用。


















