为何Go中4k个10万长度切片比同规格数组内存占用低?
Go中数组与切片作为Map值的内存差异原因
测试场景
场景1:存储固定长度数组的Map
执行以下代码创建包含4000个长度为100_000的int数组的Map:
parentMap := make(map[int][100_000]int) for i := 0; i < 4000; i++ { parentMap[i] = [100_000]int{} time.Sleep(3 * time.Millisecond) }
本地运行后内存占用超过2GB。
场景2:存储切片的Map
修改代码为存储长度为100_000的int切片的Map:
parentMap := make(map[int][]int) for i := 0; i < 4000; i++ { parentMap[i] = make([]int, 100_000) time.Sleep(3 * time.Millisecond) }
本机运行时内存峰值约为73MB。
提问者的困惑
提问者原本认为两者内存占用应大致相当,理由如下:
- 两种场景下Go运行时都会将
parentMap的值分配在堆上,避免函数退出后被回收; - 场景1直接在堆上分配4000个数组;
- 场景2在堆上分配4000个切片头,每个切片头指向堆上独立的10万长度数组;
- 两者堆上都包含4000个10万长度的数组,内存占用应相近。
查阅Go官方文档《Slices: usage and internals》后未找到相关解释,寻求差异原因。
核心原因解析
差异的本质是Go中数组是值类型,切片是引用类型,且Map的赋值逻辑放大了这一特性:
数组作为Map值的内存开销
数组是值类型,赋值操作会触发完整的元素复制。当执行parentMap[i] = [100_000]int{}时:- 首先会在堆上创建一个全新的10万长度int数组(因为数组过大无法在栈上分配);
- 然后将这个数组的所有元素完整复制到Map为该键分配的堆内存空间中;
- 复制完成前,堆上会同时存在两个完整的10万长度数组,直接导致内存占用翻倍。
此外,Map存储大值类型时,哈希桶的扩容和管理也会产生额外的内存开销,进一步推高内存使用。
切片作为Map值的内存开销
切片是引用类型,其本身仅包含3个字段(指向底层数组的指针、长度、容量),总大小仅24字节(64位系统):make([]int, 100_000)直接在堆上创建10万长度的int数组,并返回指向该数组的切片头;- 将切片赋值给Map时,仅复制这24字节的切片头到Map的哈希桶中,不会复制底层数组;
- 堆上仅存在一份底层数组,无额外复制开销,内存占用仅为4000个底层数组的大小加上可忽略的切片头内存。
关于场景2内存峰值的补充说明
你观察到的73MB内存峰值远低于理论计算值,大概率是Go运行时的GC优化导致:在循环的time.Sleep间隙,GC会及时回收临时对象,或者对于全零值的底层数组,运行时可能存在复用机制(尽管Go规范未明确保证),进一步降低了实际内存占用。
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

