Go map发生内存泄漏解决方法

目录
  • 正文
  • hamp 结构体代码
  • 查看占用的内存数量
  • 对于 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发生内存泄漏解决方法的详细内容,更多关于Go map发生内存泄漏的资料请关注我们其它相关文章!

时间: 2022-11-12

GoFrame代码优化gconv类型转换避免重复定义map

目录 前言 核心重点 优化前 优化后: 可以这么写的原因 进一步优化 批量写入 更优雅的写法如下 总结 前言 最近一直在研究 GoFrame 框架,经过一段时间的使用.总结.思考,发现确实不失为一款非常值得使用的企业级开发框架. 在我初识GoFrame教程后,曾整理过一篇文章: 非常适合PHP同学学习的GO框架:GoFrame,有兴趣的同学可以阅读一下. 今天重点讲一下我使用GoFrame的代码优化之旅. 核心重点 GoFrame几乎封装了所有能封装的东西,而我们需要做的就是在框架的基础上约定好

go数据结构和算法BitMap原理及实现示例

目录 1. BitMap介绍 如何判断数字在bit数组的位置 设置数据到bit数组 从bit数组中清除数据 数字是否在bit数组中 2. Go语言位运算 左移 右移 使用&^和位移运算来给某一位置0 3. BitMap的Go语言实现 定义 创建BitMap结构 将数据添加到BitMap 从BitMap中删除数据 判断BitMap中是否存在指定的数据 1. BitMap介绍 BitMap可以理解为通过一个bit数组来存储特定数据的一种数据结构.BitMap常用于对大量整形数据做去重和查询.在这类查

GO语言基本类型String和Slice,Map操作详解

目录 本文大纲 1.字符串String String常用操作:获取长度和遍历 字符串的strings包 字符串的strconv包: 2.切片Slice 3.集合Map 本文大纲 本文继续学习GO语言基础知识点. 1.字符串String String是Go语言的基本类型,在初始化后不能修改,Go字符串是一串固定长度的字符连接起来的字符序列,当然它也是一个字节的切片(Slice). import ("fmt") func main() { name := "Hello World

Golang编程并发工具库MapReduce使用实践

目录 环境 项目需求 mapReduce使用说明 需求实现 业务逻辑 创建任务队列 运行结果 结论 引申阅读 环境 go version go1.16.4 windows/amd64 Intel(R) Core(TM) i7-7820HK CPU @ 2.90GHz 4核心8线程 项目需求 处理数个约5MB的小文件 从源目录读取文件并拷贝到目标目录 计算源文件MD5和目标文件MD5进行对比,如不相同则报错并终止程序执行 mapReduce使用说明 go get -u github.com/tal

golang并发工具MapReduce降低服务响应时间

目录 前言 并发处理工具MapReduce MapReduce的用法演示 MapReduce使用注意事项 实现原理分析: 文末 前言 在微服务中开发中,api网关扮演对外提供restful api的角色,而api的数据往往会依赖其他服务,复杂的api更是会依赖多个甚至数十个服务.虽然单个被依赖服务的耗时一般都比较低,但如果多个服务串行依赖的话那么整个api的耗时将会大大增加. 那么通过什么手段来优化呢?我们首先想到的是通过并发来的方式来处理依赖,这样就能降低整个依赖的耗时,Go基础库中为我们提供

Go语言快速入门指针Map使用示例教程

目录 1. 指针 1.1 指针地址和指针类型 1.2 指针取值 1.3 空指针 1.4 new 的使用 1.5 new与make的区别 2. Map 2.1 什么是Map key,value存储 hash冲突 hash冲突的常见解决方法 开放定址(线性探测)和拉链的优缺点 2.2 Map 定义 2.3 map基本使用 2.4 map的遍历 2.5 map判断某个键是否存在 2.6 map使用delete()函数删除键值对 1. 指针 区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,

GoFrame gmap遍历hashmap listmap treemap使用技巧

目录 先说结论 map类型 使用技巧 基础概念 对比sync.Map 基础使用 合并 merge 序列化 过滤空值 键值对反转 Flip 出栈(随机出栈) 总结 文章比较硬核,爆肝2千多字,除了hashmap.listmap.treemap使用技巧阅读还有使用gmap的踩坑之旅,阅读大约需要5~10分钟. 先说结论 map类型 一图胜千言: 实例化示例: hashMap := gmap.New(true) listMap := gmap.NewListMap(true) treeMap := g

在Java中如何决定使用 HashMap 还是 TreeMap

HashMap简单总结: 1.HashMap 是链式数组(存储链表的数组)实现查询速度可以,而且能快速的获取key对应的value: 2.查询速度的影响因素有 容量和负载因子,容量大负载因子小查询速度快但浪费空间,反之则相反: 3.数组的index值是(key 关键字, hashcode为key的哈希值, len 数组的大小):hashcode%len的值来确定,如果容量大负载因子小则index相同(index相同也就是指向了同一个桶)的概率小,链表长度小则查询速度快,反之index相同的概率大

Java 详解Map集合之HashMap和TreeMap

目录 HashMap 创建HashMap 添加元素 访问元素 删除元素 TreeMap 创建TreeMap 添加元素 访问元素 删除元素 HashMap.TreeMap区别 Map接口储存一组成对的键-值对象,提供key(键)到value(值)的映射,Map中的key不要求有序,不允许重复.value同样不要求有序,但可以重复.最常见的Map实现类是HashMap,他的储存方式是哈希表,优点是查询指定元素效率高. Map接口被HashMap和TreeMap两个类实现. HashMap HashM

java遍历HashMap简单的方法

本文实例讲述了java遍历HashMap简单的方法.分享给大家供大家参考.具体实现方法如下: import java.util.HashMap; import java.util.Iterator; import java.util.Set; public class HashSetTest { public static void main(String[] args) { HashMap map = new HashMap(); map.put("a", "aa"

使用多种方式实现遍历HashMap的方法

今天讲解的主要是使用多种方式来实现遍历HashMap取出Key和value,首先在java中如果想让一个集合能够用for增强来实现迭代,那么此接口或类必须实现Iterable接口,那么Iterable究竟是如何来实现迭代的,在这里将不做讲解,下面主要讲解一下遍历过程. //定义一个泛型集合 Map<String, String> map = new HashMap<String, String>(); //通过Map的put方法向集合中添加数据 map.put("001&

HashMap vs TreeMap vs Hashtable vs LinkedHashMap

Map是一个重要的数据结构,本篇文章将介绍如何使用不同的Map,如HashMap,TreeMap,HashTable和LinkedHashMap. Map概览 Java中有四种常见的Map实现,HashMap,TreeMap,HashTable和LinkedHashMap,我们可以使用一句话来描述各个Map,如下: HashMap:基于散列表实现,是无序的:TreeMap:基于红黑树实现,按Key排序:LinkedHashMap:保存了插入顺序:Hashtable:是同步的,与HashMap类似

Java5种遍历HashMap数据的写法

本文介绍了最好的Java5种遍历HashMap数据的写法,分享给大家,也给自己留一个笔记,具体如下: 通过EntrySet的迭代器遍历 Iterator < Entry < Integer, String >> iterator = coursesMap.entrySet().iterator(); while (iterator.hasNext()) { Entry < Integer, String > entry = iterator.next(); System

Java中HashMap和TreeMap的区别深入理解

首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.这就是我们平时说的键值对. HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的). HashMap 非线程安全 TreeMap 非线程安全 线程安全 在Java里,线程安全一般体

Java HashMap 如何正确遍历并删除元素的方法小结

(一)HashMap的遍历 HashMap的遍历主要有两种方式: 第一种采用的是foreach模式,适用于不需要修改HashMap内元素的遍历,只需要获取元素的键/值的情况. HashMap<K, V> myHashMap; for (Map.entry<K, V> item : myHashMap.entrySet()){ K key = item.getKey(); V val = item.getValue(); //todo with key and val //WARNI

java HashMap,TreeMap与LinkedHashMap的详解

 java HashMap,TreeMap与LinkedHashMap的详解 今天上午面试的时候 问到了Java,Map相关的事情,我记错了HashMap和TreeMap相关的内容,回来赶紧尝试了几个demo理解下 package Map; import java.util.*; public class HashMaps { public static void main(String[] args) { Map map = new HashMap(); map.put("a", &