0%

结论

在Go中,map中存放map,上层map执行delete,子层map占用的内存会释放,无需手动先释放子map内存,再在上层map执行删除。

实验

在C++中,如果使用了map包含map的数据结构,当要释放上层map的某一项时,需要手动释放对应的子map占用的内存,而在Go中,垃圾回收让内存管理变得如此简单。

做2个对比实验,
实验1:普通的map,map保存到是int到int的映射,会执行delete删除map的每一项,执行垃圾回收,看内存是否被回收,map设置为nil,再看是否被回收。
实验2:map套子map,顶层map是int到子map的映射,子map是int到int的映射,同样先执行delete,再设置为nil,分别看垃圾回收情况。

实验1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"log"
"runtime"
)

var lastTotalFreed uint64
var intMap map[int]int
var cnt = 8192

func main() {
printMemStats()

initMap()
runtime.GC()
printMemStats()

log.Println(len(intMap))
for i := 0; i < cnt; i++ {
delete(intMap, i)
}
log.Println(len(intMap))

runtime.GC()
printMemStats()

intMap = nil
runtime.GC()
printMemStats()
}

func initMap() {
intMap = make(map[int]int, cnt)

for i := 0; i < cnt; i++ {
intMap[i] = i
}
}

func printMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Alloc = %v TotalAlloc = %v Just Freed = %v Sys = %v NumGC = %v\n",
m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

lastTotalFreed = m.TotalAlloc - m.Alloc
}

看结果前,解释下几个字段:

  • Alloc:当前堆上对象占用的内存大小。
  • TotalAlloc:堆上总共分配出的内存大小。
  • Sys:程序从操作系统总共申请的内存大小。
  • NumGC:垃圾回收运行的次数。

结果如下:

1
2
3
4
5
6
2018/09/29 20:09:25 Alloc = 65 TotalAlloc = 65  Just Freed = 0 Sys = 1700 NumGC = 0
2018/09/29 20:09:25 Alloc = 387 TotalAlloc = 391 Just Freed = 3 Sys = 3076 NumGC = 1
2018/09/29 20:09:25 8192
2018/09/29 20:09:25 0
2018/09/29 20:09:25 Alloc = 387 TotalAlloc = 392 Just Freed = 1 Sys = 3140 NumGC = 2
2018/09/29 20:09:25 Alloc = 74 TotalAlloc = 394 Just Freed = 314 Sys = 3140 NumGC = 3

Alloc代表了map占用的内存大小,这个结果表明,执行完delete后,map占用的内存并没有变小,Alloc依然是387,代表map的key和value占用的空间仍在map里.执行完map设置为nil,Alloc变为74,与刚创建的map大小基本是约等于。

实验2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"log"
"runtime"
)

var intMapMap map[int]map[int]int

var cnt = 1024
var lastTotalFreed uint64 // size of last memory has been freed

func main() {
// 1
printMemStats()

// 2
initMapMap()
runtime.GC()
printMemStats()

// 3
fillMapMap()
runtime.GC()
printMemStats()

// 4
log.Println(len(intMapMap))
for i := 0; i < cnt; i++ {
delete(intMapMap, i)
}
log.Println(len(intMapMap))
runtime.GC()
printMemStats()

// 5
intMapMap = nil
runtime.GC()
printMemStats()
}

func initMapMap() {
intMapMap = make(map[int]map[int]int, cnt)
for i := 0; i < cnt; i++ {
intMapMap[i] = make(map[int]int, cnt)
}
}

func fillMapMap() {
for i := 0; i < cnt; i++ {
for j := 0; j < cnt; j++ {
intMapMap[i][j] = j
}
}
}

func printMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("Alloc = %v TotalAlloc = %v Just Freed = %v Sys = %v NumGC = %v\n",
m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)

lastTotalFreed = m.TotalAlloc - m.Alloc
}

结果

1
2
3
4
5
6
7
2018/09/29 20:10:27 Alloc = 64 TotalAlloc = 64  Just Freed = 0 Sys = 1700 NumGC = 0
2018/09/29 20:10:27 Alloc = 41154 TotalAlloc = 41157 Just Freed = 3 Sys = 46026 NumGC = 5
2018/09/29 20:10:27 Alloc = 41241 TotalAlloc = 41293 Just Freed = 48 Sys = 47082 NumGC = 6
2018/09/29 20:10:27 1024
2018/09/29 20:10:27 0
2018/09/29 20:10:27 Alloc = 114 TotalAlloc = 41295 Just Freed = 41128 Sys = 47082 NumGC = 7
2018/09/29 20:10:27 Alloc = 74 TotalAlloc = 41296 Just Freed = 41 Sys = 47082 NumGC = 8

这个结果表明,在执行完delete后,顶层map占用的内存从41241降到了114,子层map占用的空间肯定是被GC回收了,不然占用内存不会下降这么显著。但依然比初始化的顶层map占用的内存64多出不少,那是因为delete操作,顶层map的key占用的空间依然在map里,当把顶层map设置为nil时,大小变为74吗,顶层map占用那些空间被释放了.

参考资料

  1. 如果这篇文章对你有帮助,不妨关注下我的Github,有文章会收到通知。
  2. 本文作者:大彬
  3. 如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2018/09/29/go-map-delete/

七牛空间的测试域名现在只能使用一个月了,有天突然发现,博客的图片都无法访问了,因此要为七牛配置自定义的域名,顺便也把博客的自定义域名配上。

看了Github Pages帮助文档,来回跳转导致配置成本增加,记录下简要配置过程,帮助打算配置自定义域名的朋友。

配置步骤

前提:

  1. 已经购买了域名。
  2. Github上已经部署了博客。

域名配置步骤,只需3个:

  1. 获取托管你的博客的Github Pages服务器。ping your_name.github.io,必然是这里列出的某一个
  2. 博客Github仓库页增加CNAME文件,CNAME的配置注意事项见这里, 我的仓库示例在这里
  3. 为你的域名配置解析,让你的DNS能指到Github Pages的服务器IP地址。如果你是阿里云的,参考这个。其中填写的IP就是步骤1中获取的Github Pages的IP地址。
阅读全文 »

Oh Shit!误删数据了。

既然看这篇文章,你必然也有rm命令误删数据的经历了,废话少说,解决办法:使用trash-cli覆盖原有的rm命令,把rm命令更改为RM

需要的软件:

  • trash-cli:会把删除的数据,单独放到程序建立的垃圾桶,可以通过自带的命令查询和恢复。

优点:再也不担心数据丢失了。
缺点:需要手动去清空垃圾桶,是否垃圾占用的空间,还好可以搞个定时任务解决。

阅读全文 »

前言

科学上网,为祖国建设添砖加瓦。

  • 教你浏览器科学上网,获取学习资料。
  • 教你终端科学上网,获取学习资料。

软件列表

  • Shadowsocks:sock5代理
  • proxychains-ng:为终端命令设置SOCKS5代理
阅读全文 »

所谓陷阱,就是它不是你认为的那样,这种认知误差可能让你的软件留下隐藏Bug。刚好Timer就有3个陷阱,我们会讲1)Reset的陷阱和2)通道的陷阱,3)Stop的陷阱与Reset的陷阱类似,自己探索吧。

Reset的陷阱在哪

Timer.Reset()函数的返回值是bool类型,我们看一个问题三连:

  1. 它的返回值代表什么呢?
  2. 我们想要的成功是什么?
  3. 失败是什么?
  • 成功:一段时间之后定时器超时,收到超时事件。
  • 失败:成功的反面,我们收不到那个事件。对于失败,我们应当做些什么,确保我们的定时器发挥作用。

Reset的返回值是不是这个意思?

阅读全文 »

前言

这篇文章从区块传播策略入手,介绍新区块是如何传播到远端节点,以及新区块加入到远端节点本地链的过程,同时会介绍fetcher模块,fetcher的功能是处理Peer通知的区块信息。在介绍过程中,还会涉及到p2p,eth等模块,不会专门介绍,而是专注区块的传播和加入区块链的过程。

当前代码是以太坊Release 1.8,如果版本不同,代码上可能存在差异。

阅读全文 »

前言

Ethash实现了PoW,PoW的精妙在于通过一个随机数确定,矿工确实做了大量的工作,并且是没有办法作弊的。接下来将介绍:

  1. Ethash的挖矿本质。
  2. Ethash是如何挖矿的。
  3. 如何验证Ethash的随机数。
阅读全文 »

前言

engine是以太坊封定义的一个接口,它的功能可以分为3类:

  1. 验证区块类,主要用在将区块加入到区块链前,对区块进行共识验证。
  2. 产生区块类,主要用在挖矿时。
  3. 辅助类。
阅读全文 »

前言

矿工在PoW中负责了产生区块的工作,把一大堆交易交给它,它生成一个证明自己工作了很多区块,然后将区块加入到本地区块链并且广播给其他节点。

接下来我们将从以下角度介绍矿工:

  1. 角色。矿工不是一个人,而是一类人,可以把这一类人分成若干角色。
  2. 一个区块产生的主要流程。
  3. 矿工的主要函数介绍,掌握矿工的主要挖矿机制。
阅读全文 »

前言

就如以太坊黄皮书讲的,以太坊是状态机,区块的产生,实际是状态迁移的过程。那以太坊

  1. 是如何定义状态的?
  2. 是如何迁移状态的?
  3. 是怎么存储状态的?

这篇文章就介绍什么是状态,以及是怎么存储的。

阅读全文 »