Go map 竟然也会发生内存泄露?
Go 程序运行时,有些场景下会导致进程进入某个“高点”,然后就再也下不来了。
比如,多年前曹大写过的一篇文章讲过,在做活动时线上涌入的大流量把 goroutine 数抬升了不少,流量恢复之后 goroutine 数也没降下来,导致 GC 的压力升高,总体的 CPU 消耗也较平时上升了 2 个点左右。
有一个 issue 讨论为什么 allgs(runtime 中存储所有 goroutine 的一个全局 slice) 不收缩,一个好处是:goroutine 复用,让 goroutine 的创建更加得便利,而这也正是 Go 语言的一大优势。
最近在看《100 mistakes》,书里专门有一节讲 map 的内存泄露。其实这也是另一个在经历大流量后,无法“恢复”的例子:map 占用的内存“只增不减”。
之前写过的一篇《深度解密 Go 语言之 map》里讲到过 map 的内部数据结构,并且分析过创建、遍历、删除的过程。
在 Go runtime 层,map 是一个指向 hmap 结构体的指针,hmap 里有一个字段 B,它决定了 map 能存放的元素个数。
hamp 结构体代码如下:
type hmap struct {
count int
flags uint8
B uint8
// ...
}
若我们想初始化一个长度为 100w 元素的 map,B 是多少呢?
用 B 可以计算 map 的元素个数:loadfactor * 2^B,loadfactor 目前是 6.5,当 B=17 时,可放 851,968 个元素;当 B=18,可放 1,703,936 个元素。因此当我们将 map 的长度初始化为 100w 时,B 的值应是 18。
loadfactor 是装载因子,用来衡量平均一个 bucket 里有多少个 key。
如何查看占用的内存数量呢?用 runtime.MemStats:
package main
import (
"fmt"
"runtime"
)
const N = 128
func randBytes() [N]byte {
return [N]byte{}
}
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}
func main() {
n := 1_000_000
m := make(map[int][N]byte, 0)
printAlloc()
for i := 0; i < n; i++ {
m[i] = randBytes()
}
printAlloc()
for i := 0; i < n; i++ {
delete(m, i)
}
runtime.GC()
printAlloc()
runtime.KeepAlive(m)
}
如果不加最后的 KeepAlive,m 会被回收掉。
当 N = 128 时,运行程序:
$ go run main2.go
0 MB
461 MB
293 MB
可以看到,当删除了所有 kv 后,内存占用依然有 293 MB,这实际上是创建长度为 100w 的 map 所消耗的内存大小。当我们创建一个初始长度为 100w 的 map:
package main
import (
"fmt"
"runtime"
)
const N = 128
func printAlloc() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}
func main() {
n := 1_000_000
m := make(map[int][N]byte, n)
printAlloc()
runtime.KeepAlive(m)
}
运行程序,得到 100w 长度的 map 的消耗的内存为:
$ go run main3.go
293 MB
这时有一个疑惑,为什么在向 map 写入了 100w 个 kv 之后,占用内存变成了 461MB?
我们知道,当 val 大小 <= 128B 时,val 其实是直接放在 bucket 里的,按理说,写入 kv 与否,这些 bucket 占用的内存都在那里。换句话说,写入 kv 之后,占用的内存应该还是 293MB,实际上却是 461MB。
这里的原因其实是在写入 100w kv 期间 map 发生了扩容,buckets 进行了搬迁。我们可以用 hack 的方式打印出 B 值:
func main() {
//...
var B uint8
for i := 0; i < n; i++ {
curB := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(*(**int)(unsafe.Pointer(&m)))) + 9))
if B != curB {
fmt.Println(curB)
B = curB
}
m[i] = randBytes()
}
//...
runtime.KeepAlive(m)
}
运行程序,B 值从 1 一直变到 18。搬迁的过程可以参考前面提到的那篇 map 文章,这里不再赘述。
而如果我们初始化的时候直接将 map 的长度指定为 100w,那内存变化情况为:
293 MB
293 MB
293 MB
当 val 小于 128B 时,初始化 map 后内存占用量一直不变。原因是 put 操作只是在 bucket 里原地写入 val,而 delete 操作则是将 val 清零,bucket 本身还在。因此,内存占用大小不变。
而当 val 大小超过 128B 后,bucket 不会直接放 val,转而变成一个指针。我们将 N 设为 129,运行程序:
0 MB
197 MB
38 MB
虽然 map 的 bucket 占用内存量依然存在,但 val 改成指针存储后内存占用量大大降低。且 val 被删掉后,内存占用量确实降低了。
总之,map 的 buckets 数只会增,不会降。所以在流量冲击后,map 的 buckets 数增长到一定值,之后即使把元素都删了也无济于事。内存占用还是在,因为 buckets 占用的内存不会少。
对于 map 内存泄露的解法:
- 重启;
- 将 val 类型改成指针;
- 定期地将 map 里的元素全量拷贝到另一个 map 里。
好在一般有大流量冲击的互联网业务大都是 toC 场景,上线频率非常高。有的公司能一天上线好几次,在问题暴露之前就已经重启恢复了,问题不大。
Go map 竟然也会发生内存泄露?的更多相关文章
- JAVA 是否会发生内存泄露(转)
原文链接: JAVA 是否会发生内存泄露 几次面试,面试官都问到了这个问题,于是搜集了答案.总结出虽然java自身有垃圾回收机制,但是很多情况下还是发生内存泄露的. java导致内存泄露的原因很明确: ...
- 如何用Java编写一段代码引发内存泄露
本文来自StackOverflow问答网站的一个热门讨论:如何用Java编写一段会发生内存泄露的代码. Q:刚才我参加了面试,面试官问我如何写出会发生内存泄露的Java代码.这个问题我一点思路都没有, ...
- ARC下的内存泄露
iOS提供了ARC功能,很大程度上简化了内存管理的代码. 但使用ARC并不代表了不会发生内存泄露,使用不当照样会发生内存泄露. 下面列举两种ARC导致内存泄露的情况. 1,循环参照 A有个属性参照B, ...
- js内存泄露的几种情况详细探讨
内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束.在C++中,因为是手动管理内存,内存泄露是经常出现的事情.而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使 ...
- js内存泄露的几种情况
想解决内存泄露问题,必须知道什么是内存泄露,什么情况下出现内存泄露,才能在遇到问题时,逐个排除.这里只讨论那些不经意间的内存泄露. 一.什么是内存泄露 内存泄露是指一块被分配的内存既不能使用,又不能回 ...
- ARC模式下的内存泄露问题
ARC模式下的内存泄露问题 iOS提供的ARC 功能很大程度上简化了编程,让内存管理变得越来越简单,但是ARC并不是说不会发生内存泄露,使用不当照样会发生. 以下列举两种内存泄露情况: 死循环造成的内 ...
- ARC下内存泄露问题
ARC下内存泄露问题 ARC下内存泄露问题,有需要的朋友可以参考下. iOS提供了ARC功能,很大程度上简化了内存管理的代码. 但使用ARC并不代表了不会发生内存泄露,使用不当照样会发生内存泄露. 下 ...
- Java内存泄露简述
Java的一个最显著的优势是内存管理.你只需要简单的创建对象而不需要负责释放空间,因为Java的垃圾回收器会负责内存的回收.然而,情况并不是这样简单,内存泄露还是经常会在Java应用程序中出现. 本篇 ...
- 项目问题总结:Block内存泄露 以及NSTimer使用问题
BLock的内存泄露 在我们代码中关于block的使用可以说随处可见,第一次接触block的时候是关于UIView的块动画,那时觉得block的使用好神奇,再后来分析总结为block其实就是一个c语言 ...
随机推荐
- java方法---可变参数
可变参数 在方法的声明中,在指定参数类型后面加一个...(省略号) 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数,任何普通参数必须在它之前声明:
- node前后端交互(Express)
1. Express框架是什么 1.1 Express是一个基于Node平台的web应用开发框架,它提供了一系列的强大特性,帮助你创建各种Web应用.我们可以使用 npm install expres ...
- 关于 Math.random()生成指定范围内的随机数的公式推导
关于 Math.random()生成指定范围内的随机数的公式推导 在 java 中,用于生成随机数的 Math 方法 random()只能生成 0-1 之间的随机数,而对于生成指定区间,例如 a-b ...
- Html飞机大战(三):定义状态
好家伙, 1.为飞机大战定义状态 1.开始 START 有一个飞机大战LOGO &天空 2.开始时 STRATING 有一个飞机加载的界面&天空 3.运行时 RUNNING 我方飞机& ...
- KingbaseES 工具sys_dump,sys_restore使用介绍
说明: KingbaseES V8R6版本中自带数据库备份导出sys_dump,和备份恢复sys_restore工具. sys_dump:把KingbaseES数据库抽取为一个脚本文件或其他归档文件. ...
- 国产PLM软件在创新实践中强势崛起
近日,"璞华PLM"先后获得微度医疗.埃特斯等多个客户的订单,即使在疫情环境下也展现出了强劲的高速增长.在产品生命周期管理(PLM,Product Lifecycle Manage ...
- 《网页设计基础——CSS的四种引入方式详解》
网页设计基础--CSS的四种引入方式详解 一.行内式: 规则: 1. 行内式是所有样式方法中最为直接的一种,它直接对HTML的标记使用style属性,然后将CSS代码直接写在其中. 格 ...
- JAVA中容器设计的进化史:从白盒到黑盒,再到跻身为设计模式之一的迭代器
大家好,又见面了. 在我们的项目编码中,不可避免的会用到一些容器类,我们可以直接使用List.Map.Set.Array等类型.当然,为了体现业务层面的含义,我们也会根据实际需要自行封装一些专门的Be ...
- Elasticsearch 主从同步之跨集群复制
文章转载自:https://mp.weixin.qq.com/s/alHHxXont6XFm_m9PfsGfw 1.什么是跨集群复制? 跨集群复制(Cross-cluster replication, ...
- 在CentOS 8服务器上搭建FastDFS环境
什么是FastDFS? 这里,我就摘录下百度百科上对于FastDFS的描述. FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储.文件同步.文件访问(文件上传.文件下 ...