听说过对 Go map 做 GC 吗?

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

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

所以一般的做法:就是 **重建map**。而 `go-zero` 中内置了 `safemap` 的容器组件。`safemap` 在一定程度上可以避免这种情况发生。

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

## 原生map删除

```go
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"` :

```go
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`。

这些函数的参数类型是具体的 `int64`,`mapdelete_fast64` 跟原始的 `delete` 操作一样的,所以我们来看看 `mapdelete`。

### mapdelete

*长图预警!!!*

![](https://oscimg.oschina.net/oscnet/up-8c0678b8fb76fd4fd92c8d78bba0264aa53.png)

大致代码分析如上,具体代码就留给大家去阅读了。其实大致过程:

1. 写保护,防止并发写
2. 查询要删除的 `key` 是否存在
3. 存在则对其标志做删除标记
4. `count--`

所以你在大面积删除 `key` ,实际 `map` 存储的 `key` 是不会删除的,只是标记当前的key状态为 `empty`。

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

但是这个对有些场景是不妥的,如果开发者在未来时间内都不会再插入相同的 `key` ,很可能会导致 `OOM`。

所以针对以上情况,`go-zero` 开发了 `safemap` 。下面我们看看 `safemap` 是如何避免这个问题的?

## safemap

直接从操作 `safemap` 中分析为什么要这么设计:

![](https://oscimg.oschina.net/oscnet/up-a7dcc57de4e1147914fb94b4780051b36ed.png)

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://oscimg.oschina.net/oscnet/up-39144e1062406504c8ef5884281f6794ee8.png)

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

更多实现细节,可以查看源码!

## 项目地址

[https://github.com/tal-tech/go-zero](https://github.com/tal-tech/go-zero)

欢迎使用 go-zero 并 **star** 支持我们!

## 微信交流群

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值