Go map 内存泄露

article/2024/7/24 1:54:11

前言

Go中, map这个结构使用的频率还是比较高的. 其实在所有的语言中, map使用的频率都是很高的.

之前在使用中, 一直都知道map的内存在元素删除的时候不会回收, 但一直没有仔细的研究为什么. 今天就来好好揣摩揣摩.

func main() {m := make(map[int][128]byte)for i := 0; i < 100000; i++ {b := [128]byte{}for j := 0; j < 128; j++ {b[j] = byte(j + 1)}m[i] = b}// 打印堆内存var ms runtime.MemStatsruntime.ReadMemStats(&ms)fmt.Printf("堆内存: %d B, map size: %d\n", ms.Alloc, len(m))// 释放 mapfor i := 0; i < 100000; i++ {delete(m, i)}runtime.ReadMemStats(&ms)fmt.Printf("堆内存(释放后): %d B, map size: %d\n", ms.Alloc, len(m))// 手动触发 GCruntime.GC()runtime.ReadMemStats(&ms)fmt.Printf("堆内存(GC): %d B, map size: %d\n", ms.Alloc, len(m))// 保存 map 的引用,防止 GC 回收runtime.KeepAlive(m)
}

老规矩, 还是先来说说是个什么现象(本文所有例子, 基于 Go1.18)). 如果你运行这个程序, 那么会得到这样的结果:

堆内存: 32197640 B, map size: 100000
堆内存(释放后): 32198752 B, map size: 0
堆内存(GC): 21113120 B, map size: 0

可以看到, 再将map内容清空后, 运行GC, 内存占用仍高达20M. 而这个现象, 就是在前面提到过的, Go中的map内存占用只会增加不会减少.

探究

gdb 分析

为了知道mapGo中的具体实现, 我通过gdbm的类型打印了出来, 这是ptype m的结果:

type = struct hash<int,[128]uint8> {
int count;
uint8 flags;
uint8 B;
uint16 noverflow;
uint32 hash0;
bucket<int,[128]uint8> *buckets;
bucket<int,[128]uint8> *oldbuckets;
uintptr nevacuate;
runtime.mapextra *extra;
}

hash结构体明显是编辑器编译过后的, 为了方便, 我直接在源码中通过字段名搜索, 果然找到了字段一模一样的结构体hmap, 此结构体位于runtime/map.go文件中.

再使用print *m命令查看不同阶段结构体中的内容:

初始化之前:

{count = 0, flags = 0 ‘\000’, B = 0 ‘\000’, noverflow = 0, hash0 = 2403831944, buckets = 0xc00013e380, oldbuckets = 0x0, nevacuate = 0, extra = 0x0}

初始化之后:

{count = 100000, flags = 0 ‘\000’, B = 14 ‘\016’, noverflow = 2679, hash0 = 4095224677, buckets = 0xc001540000, oldbuckets = 0x0, nevacuate = 8192, extra = 0xc00012c018}

delete 所有内容之后:

{count = 0, flags = 0 ‘\000’, B = 14 ‘\016’, noverflow = 2679, hash0 = 112371461, buckets = 0xc001540000, oldbuckets = 0x0, nevacuate = 8192, extra = 0xc00012c018}

GC 之后:

{count = 0, flags = 0 ‘\000’, B = 14 ‘\016’, noverflow = 2679, hash0 = 112371461, buckets = 0xc001540000, oldbuckets = 0x0, nevacuate = 8192, extra = 0xc00012c018}

可以看到, 删除所有内容之后, 只有count的值发生了变化. 分析到这里, 就必须要看一下map是如果实现的了,

map原理

如果你对JavaHashMap的实现有了解, 那么这里也一样, 数组+链表来实现hash表.

在内存的分布上, map大致是这样的一个结构:

image-20230325162200036

其中每个Bucket都可以保存8个KV, 数据在存放的时候, 会根据hash函数的结果得出在Bucket 列表的偏移量, 然后将值放到对应的位置.

overflow Bucket是当Bucket自身存放不下时, 与其组成链表来容纳更多数据

至于Bucket结构体为什么要将K/V分开放, 在源码中也给出解释了, 如果将K/V放到一起, 遇到map[int8]int64这样的, 就会遇到内存对齐的问题, 浪费一部分内存.

插入

插入操作通过调用mapassign函数, 其大体步骤如下:

  1. 使用hash 值定位元素在哪一个 Bucket 中
  2. 遍历 Bucket 中的元素, 找到第一个空位, 将数据插入

如何处理 hash 冲突

殊途同归, Go中已然是用链表来解决hash冲突的.

我们不是通过 hash 来确定了元素存放在哪一个bucket中嘛. 其实, 每一个Bucket就是一个链表. 它的extraBucket字段用来链接到链表的下一个元素. 只不过这个链表中, 每个元素都可以存放8个K/V.

如何快速找到空位

遍历Bucket内容时, 为了快速定位, 加了一个小小的缓存, 会将keyhash值高8位存起来, 用于快速比较是否相等.

扩容机制

Go中, map每次扩容都会将原来的容量乘2, 也是有一个指数因子来判断是否需要扩容. 大差不差.

查看修改的操作, 在这里就不赘述了, 按照插入的流程寻找元素即可.

删除

map元素删除的操作十分简单, 可以看下源码实现. 简单说来就是:

  1. 找到元素
  2. key/value的内容清空
  3. 将长度count减1

结构体

简单介绍下hmap各个字段的含义:

type hmap struct {count     int // 当前 map 中元素个数, len 函数用的就是它flags     uint8 // 标志位B         uint8  // 指数, 标识当前桶的个数为 2^Bnoverflow uint16 // 溢出桶的大致数目hash0     uint32 // 随机种子buckets    unsafe.Pointer // Bucket 数组指针oldbuckets unsafe.Pointer // 数组扩容时迁移过程中指向就地址的nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)extra *mapextra // 用来组成 Bucket 链表的内容
}
type mapextra struct {overflow    *[]*bmap // 指向溢出Bucket的地址oldoverflow *[]*bmap // 同上, 迁移过程使用nextOverflow *bmap // 指向第一个空闲的 Bucket, 追加时可快速获取到
}

解惑

现在对Go中的map有了一定的了解了, 再回来看最开始的问题, 为什么内存没有被回收呢? 很简单, 删除元素的时候, 仅仅是将key/value内容置空, 但map占用的内存仍然没有释放. 删除后再向map中添加数据, 是可以使用已经清空内存的.

也就是说, 在将数据从map中删除的时候, 仅仅是map自身的内存没有被回收, value中存放的如果是一个结构体, 那么是不影响结构体本身GC的.

为了验证这个猜想, 你可以将最开始例子中的map换成map[int][1024]bytemap[int]*[128]byte, 再次运行就会发现, GC后内存占用明显下降了. 更换指针很容易理解, 增大value的内存占用, 也会让Go在编译期将其转为指针类型.

解决

到这里, 我们知道了map自身的内存占用只增不减, 也知道了为什么会出现这个问题.

那么, 如何解决呢? 如果不进行解决, 在某一个流量高峰期, map中保存了大量的数据, 后面流量降下来了, 但是map的内存占用会居高不下.

我简单想了几种方案:

  1. 定期备份. 每个一段时间, 将整个map拷贝一份到新的map
  2. value使用指针类型, 这样map中保留的内存仅为指针所占空间, 与value大小无关. 而value的对象是会被GC回收的. 我简单测试了下, map[int]*[128]byte类型的map, 100w 元素, 全部删除后GC, 内存占用(map自身)仅为38M.

当然了, 肯定还有很多花里胡哨的解决方案, 比如使用多个小map等等, 但这2种方案应该已经能够解决日常开发的问题了.


原文地址: https://hujingnb.com/archives/894


http://www.ngui.cc/article/show-1007528.html

相关文章

Python参数类型定义、私有函数与变量、全局变量

函数的参数类型定义 参数名 冒号 类型函数函数定义在Python3.7之后可用函数不会对参数类型进行验证 def add(a:int, b:int3):print(a b)add(1, 2) add(hello, xiaomu)def test(a:int, b:int3, *args:int, **kwargs):print(a, b, args, kwargs)test(1, 2, 3, 4, namexiaomu)…

用户态--fork函数创建进程

我们一般使用Shell命令行来启动一个程序&#xff0c;其中首先是创建一个子进程。但是由于Shell命令行程序比较复杂&#xff0c;为了便于理解&#xff0c;我们简化了Shell命令行程序&#xff0c;用如下一小段代码来看怎样在用户态创建一个子进程。 #include <stdio.h> #i…

OceanBase CTO杨传辉:持续降低使用门槛,打造开发者友好的分布式数据库

3月25日&#xff0c;首届OceanBase开发者大会在北京举行。大会发布了OceanBase 4.1版本&#xff0c;公布两大友好工具&#xff0c;升级文档易用性&#xff0c;统一企业版和社区版代码分支&#xff0c;全面呈现了OceanBase打造极致的开发者友好数据库的成果。过去13年&#xff0…

【广州华锐互动】电力线路检测VR实训系统有哪些特色?

在电力系统运行中&#xff0c;故障测试是非常重要的一环&#xff0c;旨在检测和排除系统中可能存在的故障&#xff0c;保障电力系统的正常运行。传统的故障测试方法往往需要在实际场景下进行操作&#xff0c;不仅操作难度较大&#xff0c;而且存在安全隐患&#xff0c;同时操作…

详解内核态与用户态

介绍下内核态与用户态 内核态和用户态是操作系统中的两种不同的运行状态&#xff0c;它们的区别如下&#xff1a; 权限不同&#xff1a;内核态是操作系统拥有最高权限的运行状态&#xff0c;可以访问系统的所有资源&#xff0c;而用户态只能访问受限的资源。 系统调用&#x…

网易云音乐API部署Vercel获取接口过程

前提&#xff1a;部署自己的网易云接口主要用途在于在完成前端的仿网易云播放器的时候&#xff0c;根据自己部署的接口可以用于获取数据。大体流程是通过在github上fork别人的API接口项目&#xff0c;然后在Vercel部署即可获得自己的网易云后端数据接口了&#xff0c;不过根据我…

(五)Tomcat源码阅读:Connector组件分析

一、概述 因为Connector组件没有实现接口规范&#xff0c;因此我们直接对该类的方法进行分析即可。 二、源码阅读 阅读思路&#xff0c;我的阅读思路是这样的&#xff0c;大的类无非就是对小类的使用&#xff0c;因此我们想分析整体的一下架构的化我们就先从大类出发找到比较…

【LeetCode】二叉树的前序遍历(递归,迭代,Morris 遍历)

目录 题目要求&#xff1a;给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 方法一&#xff1a;递归 方法二&#xff1a;迭代 思路分析&#xff1a; 复杂度分析 代码展示&#xff1a; 方法三&#xff1a;迭代进阶 思路分析&#xff1a; 代码展示&…

OpenAI translator + AI vocabulary + scispace + chatpdf 学英语

OpenAI translator AI vocabulary scispace chatpdf 学英语 文章目录OpenAI translator AI vocabulary scispace chatpdf 学英语workflow记录这几个软件都是这波AIGC热潮中的产品下面我要读一下2022美赛E题我分别上传到了chatpdf和scispace&#xff0c; 需要翻译的地方我…

RHCSA8.2模拟题

环境系统IP 地址mars.domain250.example.com172.25.250.100venus.domain250.example.com172.25.250.200您使用的系统属于 DNS 域 domain250.example.com。该域中的所有系统都位于172.25.250.0/255.255.255.0 子网中&#xff0c;该子网中的所有系统都位于 domain250.example.com…