关于golang:听说过对-Go-map-做-GC-吗

42次阅读

共计 2082 个字符,预计需要花费 6 分钟才能阅读完成。

在 Golang 中的 map 构造,在删除键值对的时候,并不会真正的删除,而是标记。那么随着键值对越来越多,会不会造成大量内存节约?

首先答案是会的,很有可能导致 OOM,而且针对这个还有一个探讨:https://github.com/golang/go/issues/20135。大抵的意思就是在很大的 map 中,delete 操作没有真正开释内存而可能导致内存 OOM。

所以个别的做法:就是 重建 map。而 go-zero 中内置了 safemap 的容器组件。safemap 在肯定水平上能够防止这种状况产生。

那首先咱们看看 go 原生提供的 map 是怎么删除的?

原生 map 删除

1  package main
2
3  func main() {4      m := make(map[int]string, 9)
5      m[1] = "hello"
6      m[2] = "world"
7      m[3] = "go"
8
9      v, ok := m[1]
10     _, _ = fn(v, ok)
11
12     delete(m, 1)
13  }
14
15 func fn(v string, ok bool) (string, bool) {
16     return v, ok
17 }

测试代码如上,咱们能够通过 go tool compile -S -N -l testmap.go | grep "CALL"

0x0071 00113 (test/testmap.go:4)        CALL    runtime.makemap(SB)
0x0099 00153 (test/testmap.go:5)        CALL    runtime.mapassign_fast64(SB)
0x00ea 00234 (test/testmap.go:6)        CALL    runtime.mapassign_fast64(SB)
0x013b 00315 (test/testmap.go:7)        CALL    runtime.mapassign_fast64(SB)
0x0194 00404 (test/testmap.go:9)        CALL    runtime.mapaccess2_fast64(SB)
0x01f1 00497 (test/testmap.go:10)       CALL    "".fn(SB)
0x0214 00532 (test/testmap.go:12)       CALL    runtime.mapdelete_fast64(SB)
0x0230 00560 (test/testmap.go:7)        CALL    runtime.gcWriteBarrier(SB)
0x0241 00577 (test/testmap.go:6)        CALL    runtime.gcWriteBarrier(SB)
0x0252 00594 (test/testmap.go:5)        CALL    runtime.gcWriteBarrier(SB)
0x025c 00604 (test/testmap.go:3)        CALL    runtime.morestack_noctxt(SB)

执行第 12 行的 delete,理论执行的是 runtime.mapdelete_fast64

这些函数的参数类型是具体的 int64mapdelete_fast64 跟原始的 delete 操作一样的,所以咱们来看看 mapdelete

mapdelete

长图预警!!!

大抵代码剖析如上,具体代码就留给大家去浏览了。其实大抵过程:

  1. 写爱护,避免并发写
  2. 查问要删除的 key 是否存在
  3. 存在则对其标记做删除标记
  4. count--

所以你在大面积删除 key,理论 map 存储的 key 是不会删除的,只是标记以后的 key 状态为 empty

其实出发点,和 mysql 的标记删除相似,避免后续会有雷同的 key 插入,省去了扩缩容的操作。

然而这个对有些场景是不妥的,如果开发者在将来工夫内都不会再插入雷同的 key,很可能会导致 OOM

所以针对以上状况,go-zero 开发了 safemap。上面咱们看看 safemap 是如何防止这个问题的?

safemap

间接从操作 safemap 中剖析为什么要这么设计:

  1. 预设一个 删除阈值,如果触发会放到一个新预设好的 newmap
  2. 两个 map 是一个整体,所以 key 只能留一份

所以为什么要设置两个 map 就很分明了:

  1. dirtyOld 作为存储主体,如果 delete 操作达到阈值,则会触发迁徙。
  2. dirtyNew 作为暂存体,会在达到阈值时,寄存局部 key/value

所以在迁徙操作时,咱们须要做的就是:将原先的 dirtyOld 清空,存储的 key/value 通过 for-range 从新存储到 dirtyNew,而后将 dirtyNew 指向 dirtyOld

可能会有疑难:不是说 key/value 没有删除吗,只是标记了 tophash=empty

其实在 for-range 过程中,会过滤掉 tophash <= emptyOne 的 key

这样就实现了不须要的 key 不会被退出到 dirtyNew,进而不会影响 dirtyOld

这其实也就是垃圾回收的年轻代和新生代的概念。

更多实现细节,能够查看源码!

我的项目地址

https://github.com/tal-tech/go-zero

欢送应用 go-zero 并 star 反对咱们!

微信交换群

关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。

正文完
 0