对于频繁增删map的场景,我们很关心map的内存是否会自动释放。

先说结论:
  • 如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放

  • 如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用

  • 将map设置为nil后,内存被回收

实验

以子元素是整形和map为例

重要一点:

申请一个全局map来保证内存被分配到堆上面
原因请参照:golang的gc回收针对堆还是栈?变量内存分配在堆还是栈?

实验1

普通的map,map保存到是int到int的映射,会执行delete删除map的每一项,执行垃圾回收,看内存是否被回收,map设置为nil,再看是否被回收。

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:垃圾回收运行的次数。

结果如下:

2019/12/19 11:48:03 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 11:48:03 Alloc = 403 TotalAlloc = 437  Just Freed = 33 Sys = 3234 NumGC = 1
2019/12/19 11:48:03 8192
2019/12/19 11:48:03 0
2019/12/19 11:48:03 Alloc = 404 TotalAlloc = 438  Just Freed = 1 Sys = 3234 NumGC = 2
2019/12/19 11:48:03 Alloc = 91 TotalAlloc = 439  Just Freed = 313 Sys = 3234 NumGC = 3

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

实验二

map套子map,顶层map是int到子map的映射,子map是int到int的映射,同样先执行delete,再设置为nil,分别看垃圾回收情况。

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
}

2019/12/19 11:49:59 Alloc = 89 TotalAlloc = 89  Just Freed = 0 Sys = 1700 NumGC = 0
2019/12/19 11:50:00 Alloc = 41171 TotalAlloc = 41204  Just Freed = 32 Sys = 46026 NumGC = 5
2019/12/19 11:50:00 Alloc = 41259 TotalAlloc = 41342  Just Freed = 49 Sys = 46026 NumGC = 6
2019/12/19 11:50:00 1024
2019/12/19 11:50:00 0
2019/12/19 11:50:00 Alloc = 132 TotalAlloc = 41343  Just Freed = 41129 Sys = 46026 NumGC = 7
2019/12/19 11:50:00 Alloc = 91 TotalAlloc = 41344  Just Freed = 41 Sys = 46026 NumGC = 8

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

参考资料:

  • https://blog.csdn.net/m0_43499523/article/details/85851478
  • http://blog.cyeam.com/json/2017/11/02/go-map-delete