flycloud
V2EX  ›  Go 编程语言

golang 内存回收的疑问

By flycloud at 2021 年 9 月 7 日 · 5238 次点击

先贴代码:

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func main() {
	data := make(map[int32][]int32)
	for i := 0; i < 1024; i++ {
		msg := make([]int32, 1024 * 512, 1024 * 512)
		msg[0] = 0	//访问一下内存, 触发从内核真正分配内存
		data[int32(i)] = msg
	}
	fmt.Println(len(data))
    
	if true {
		sig := make(chan os.Signal, 1)
		signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
		<-sig
	}
}

编译:

GODEBUG=madvdontneed=1 GOOS=linux GOARCH=amd64 go build

如上,分配了 1024 个内存占用 2MB 的 slice,放入了 map 中,总共 2GB 内存占用。程序启动后分配完就一直阻塞着,大概 3 分钟后内存占用从 2GB 多降低到 70MB 左右,表现上看是之前分配的 slice 被 gc 了。但是 map 没有删除操作,也没有置为 nil,难道 golang 的 gc 机制就是这样,发现后续没有再使用这个 map 就直接 gc 了,尽管还没有离开这个 map 所在的作用域?

image

40 条回复  •  2021-09-11 01:50:36 +08:00
Mohanson
   1
Mohanson  
   2021 年 9 月 7 日
靠作用回收内存的手段叫 RAII (c++, rust), Go 用的是引用计数, 原理不一样.
gamexg
   2
gamexg  
   2021 年 9 月 7 日
在


```
if true {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}
```

后加个 fmt.Print(data) 试试。
data 没在使用,编译器可能回优化掉了。
CRVV
   3
CRVV  
   2021 年 9 月 7 日
> 难道 golang 的 gc 机制就是这样,发现后续没有再使用这个 map 就直接 gc 了,尽管还没有离开这个 map 所在的作用域?

对的,就是这样。
MoYi123
   4
MoYi123  
   2021 年 9 月 7 日
msg[0] = 0;

改成
for ii, _ := range msg {
msg[ii] = 0 //访问一下内存, 触发从内核真正分配内存
}

就是 2G 内存了

我感觉是 msg[0]这样写是只取了一页的内存,所以还有 70MB,要是 map 被 gc 了,应该不会用这多内存的。
flycloud
   5
flycloud  
OP
   2021 年 9 月 7 日
@gamexg 在阻塞代码之后再使用 data,内存肯定不会降低的(已验证)。所以肯定是因为 map 被 gc 了。

但是为什么是 3 分钟后内存才瞬间降低, 然后就一直占有着 70MB,就比较奇怪了。
flycloud
   6
flycloud  
OP
   2021 年 9 月 7 日
@MoYi123 效果是一样的,msg[0] = 0 只访问这一个数据,RES 内存就是 2GB,说明访问了之后就分配了全部的内存,而不是只分配了一页。
MoYi123
   7
MoYi123  
   2021 年 9 月 7 日
@flycloud 我跑这个代码的表现和你的完全不同。阻塞代码之后再使用 data,我这里也是 70MB.
flycloud
   8
flycloud  
OP
   2021 年 9 月 7 日
@MoYi123 代码确定是这样的么:
```
func main() {
data := make(map[int32][]int32)
for i := 0; i < 1024; i++ {
msg := make([]int32, 1024 * 512, 1024 * 512)
msg[0] = 0 //访问一下内存, 触发从内核真正分配内存
data[int32(i)] = msg
}
fmt.Println(len(data))

if true {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}
fmt.Println(len(data))
}
```
一直阻塞着,我等了 10 分钟,还是 2GB 的内存。
MoYi123
   9
MoYi123  
   2021 年 9 月 7 日
是的,我环境是
go version go1.17 linux/amd64

Linux ubuntu 5.11.0-27-generic #29~20.04.1-Ubuntu SMP Wed Aug 11 15:58:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

windows 上也是一样。
jtgogogo
   10
jtgogogo  
   2021 年 9 月 7 日
我这边一直都是 72M
flycloud
   11
flycloud  
OP
   2021 年 9 月 7 日
@MoYi123 我的是:
go version go1.17 darwin/amd64
运行环境是:centos,Linux 172-20-245-36 4.18.0-193.28.1.el8_2.x86_64

你的运行结果更神奇了啊,阻塞后还会使用 data 的被 gc 了导致内存降低了?
jtgogogo
   12
jtgogogo  
   2021 年 9 月 7 日
MAC
PureWhiteWu
   13
PureWhiteWu  
   2021 年 9 月 7 日
@flycloud 3 分钟后才降低是 sysmon 每两分钟执行一次强制 gc 导致的。
PureWhiteWu
   14
PureWhiteWu  
   2021 年 9 月 7 日
@Mohanson go 不是引用计数,是三色标记法
flycloud
   15
flycloud  
OP
   2021 年 9 月 7 日
@jtgogogo 是的,我在 mac 下运行一直是 70MB 。

但是在 centos 下,编译运行,刚开始是 2GB,几分钟后降低为 70MB 。
flycloud
   16
flycloud  
OP
   2021 年 9 月 7 日
@PureWhiteWu 嗯,可以明确的是 data 被 gc 了,但是剩下的 70MB 是哪儿去了呢
tuxz
   17
tuxz  
   2021 年 9 月 7 日
请问这种图是怎么生成的呢
ksco
   18
ksco  
   2021 年 9 月 7 日
不管是什么垃圾回收算法,一定是根据内存是否还被引用来判断是否应该被回收。data 在程序结束前一直保持着对 map 的引用,所以是不会被 GC 的。所以 data 一定不是被 “GC” 了。

我猜测是因为你的程序中只用到了一个大 slice 的一小部分,所以没有用到的部分可能是被 Go 优化器回收了?不过这个就纯属拍脑袋瞎猜了。
flycloud
   20
flycloud  
OP
   2021 年 9 月 7 日
@ksco 不是这样哈,分配出来的 slice,全部遍历了,也是一样的结果。
MrKrabs
   21
MrKrabs  
   2021 年 9 月 7 日
编译器优化掉了吧
flycloud
   22
flycloud  
OP
   2021 年 9 月 7 日
应该是 gc 机制如此。

GODEBUG=madvdontneed=1 go build -gcflags="-N -l"
关闭了编译器优化,内存还是降低了。
gamexg
   23
gamexg  
   2021 年 9 月 7 日
@flycloud #5
印象 go 内存回收是有一个独立线程执行的,
按照一定的策略定时执行,策略具体细节记不清,印象是新增内存达到一定比例或达到一定时间。
可以运行 runtime.GC() 来手动触发内存回收,可能需要手动调用多次才能完全释放。
ksco
   24
ksco  
   2021 年 9 月 7 日
@flycloud

> 应该是 gc 机制如此。

GC 实现不了回收一个还在被引用的内存,因为这需要 GC 有预测未来的能力,这是不可能的。


> 不是这样哈,分配出来的 slice,全部遍历了,也是一样的结果。

你确定吗?我在 macOS 下试了一下下面这段代码,过了十分钟也还是 2G 内存

func main() {
data := make(map[int32][]int32)
for i := 0; i < 1024; i++ {
msg := make([]int32, 1024 * 512, 1024 * 512)
for j:=0;j<1024*512;j++ {
msg[j] = rand.Int31()
}
data[int32(i)] = msg
}
fmt.Println(len(data))

if true {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}
}
flycloud
   25
flycloud  
OP
   2021 年 9 月 7 日
@gamexg 真大佬,确实啊,调用 2 次 runtime.GC() 内存就马上降低了,一次还不行。

func main() {
data := make(map[int32][]int32)
for i := 0; i < 1024; i++ {
msg := make([]int32, 1024 * 512, 1024 * 512)
msg[0] = 0 //访问一下内存, 触发从内核真正分配内存
data[int32(i)] = msg
}
fmt.Println(len(data))
runtime.GC()
runtime.GC()

if true {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
}
}
flycloud
   26
flycloud  
OP
   2021 年 9 月 7 日
@ksco 看你的 go 版本,不同版本内存回收策略不一样。编译时强制使用 madvdontneed 。

GODEBUG=madvdontneed=1 go build
ksco
   27
ksco  
   2021 年 9 月 7 日
你运行一下上面我贴的代码试试呗,看会不会被 GC 。
flycloud
   28
flycloud  
OP
   2021 年 9 月 7 日
@ksco

centos 下表现一样,过几分钟后 GC,瞬间降低到 70MB 。
mac 下过几分钟后开始内存慢慢下降。
darrh00
   29
darrh00  
   2021 年 9 月 7 日
@flycloud #26

GODEBUG 是个运行时环境变量,编译时指定只影响 go build 本身,不会影响编译出来的程序。
flycloud
   30
flycloud  
OP
   2021 年 9 月 7 日
@darrh00 感谢指正
codehz
   31
codehz  
   2021 年 9 月 7 日
“访问一下内存, 触发从内核真正分配内存”这个,内核也不知道你数据结构有多大啊。。。
然后为啥一开始会吃 2G 呢,那多半是 go 使用 mmap 的 populate 选项了,这个选项能保证立即分配所有内存,但是不保证之后就不回收回去鸭*
Orlion
   32
Orlion  
   2021 年 9 月 7 日
进到 swap 了?
Sasasu
   33
Sasasu  
   2021 年 9 月 7 日
https://i.loli.net/2021/09/07/VgxG4OJyUjY6pDa.jpg

GC trace 显示在第二次 force GC 之后 Go 编译器预知未来了,直接把你的数据干掉了。
我认为是某种 UB,Go 判定的的函数已经返回了。

为什么刚好是 250s ( 4.1 分钟),是因为 Go 每隔 120s 会触发一次 force GC,这个等待时间不随机不可调。
ksco
   34
ksco  
   2021 年 9 月 7 日
@Sasasu @flycloud

是,编译器确实是在编译阶段就分析(预测)出了 data 不会再被使用了,然后直接 GC 掉了。

使用楼主主贴中给出的程序,运行 go build -gcflags '-live',可以看到编译器知道在 fmt.Println(len(data)) 这行之后 data 再也不会被使用了。

看来变量的 liveness 和 scope 并不一定是一致的。
jeeyong
   35
jeeyong  
   2021 年 9 月 7 日
瞎猜一下, 是不是在把东西写到 swap 里?
vindurriel
   36
vindurriel  
   2021 年 9 月 8 日 via iPhone
GODEBUG=gctrace=1
bruce0
   37
bruce0  
   2021 年 9 月 8 日
猜测一下,会不会是程序一开始就只给 slice 分配了 70M,但是 go 的 runtime 向操作系统申请了 2G 内存,未使用的部分(2G-70M)存在 HeapIdle 区中,因为长时间没有使用,HeapIdle 中的内存又归还给操作系统了
flycloud
   38
flycloud  
OP
   2021 年 9 月 8 日   ❤️ 1
破案了,开了 pprof,可以看到各项内存占用情况。剩下的那 70MB 是垃圾回收标记元信息使用的内存:GCSys 。

/doge

data gc 前:
```
heap profile: 286: 599785472 [286: 599785472] @ heap/1048576
286: 599785472 [286: 599785472] @ 0x696898 0x43bdf6 0x4726e1
# 0x696897 main.main+0xf7 /data/gowork/src/test/test.go:21
# 0x43bdf5 runtime.main+0x255 /usr/local/go/src/runtime/proc.go:225


# runtime.MemStats
# Alloc = 2147822008
# TotalAlloc = 2148011064
# Sys = 2291062280
# Lookups = 0
# Mallocs = 2741
# Frees = 671
# HeapAlloc = 2147822008
# HeapSys = 2214199296
# HeapIdle = 65634304
# HeapInuse = 2148564992
# HeapReleased = 65150976
# HeapObjects = 2070
# Stack = 393216 / 393216
# MSpan = 176528 / 180224
# MCache = 4800 / 16384
# BuckHashSys = 1444089
# GCSys = 73675560
# OtherSys = 1153511
# NextGC = 2403760736
# LastGC = 1631066553972785432
```

data gc 后:
```
heap profile: 0: 0 [1007: 2111832064] @ heap/1048576
0: 0 [1007: 2111832064] @ 0x696898 0x43bdf6 0x4726e1
# 0x696897 main.main+0xf7 /data/gowork/src/test/test.go:21
# 0x43bdf5 runtime.main+0x255 /usr/local/go/src/runtime/proc.go:225


# runtime.MemStats
# Alloc = 211840
# TotalAlloc = 2148063184
# Sys = 2291062280
# Lookups = 0
# Mallocs = 2895
# Frees = 2059
# HeapAlloc = 211840
# HeapSys = 2214133760
# HeapIdle = 2213347328
# HeapInuse = 786432
# HeapReleased = 2213289984
# HeapObjects = 836
# Stack = 458752 / 458752
# MSpan = 45288 / 180224
# MCache = 4800 / 16384
# BuckHashSys = 1444089
# GCSys = 73694016
# OtherSys = 1135055
# NextGC = 4194304
# LastGC = 1631066839647887815
```

各项指标含义:
```
Alloc uint64 //golang 语言框架堆空间分配的字节数
TotalAlloc uint64 //从服务开始运行至今分配器为分配的堆空间总 和,只有增加,释放的时候不减少
Sys uint64 //总共从 OS 申请的字节数,它是虚拟内存空间,不一定全部映射成了物理内存
Lookups uint64 //被 runtime 监视的指针数
Mallocs uint64 //服务 malloc heap objects 的次数
Frees uint64 //服务回收的 heap objects 的次数
HeapAlloc uint64 //服务分配的堆内存字节数
HeapSys uint64 //系统分配的作为运行栈的内存
HeapIdle uint64 //申请但是未分配的堆内存或者回收了的堆内存(空闲)字节数
HeapInuse uint64 //正在使用的堆内存字节数
HeapReleased uint64 //返回给 OS 的堆内存,类似 C/C++中的 free 。
HeapObjects uint64 //堆内存块申请的量
GCSys uint64 //垃圾回收标记元信息使用的内存
OtherSys uint64 //golang 系统架构占用的额外空间
NextGC uint64 //垃圾回收器检视的内存大小
LastGC uint64 // 垃圾回收器最后一次执行时间。
````
tuxz
   39
tuxz  
   2021 年 9 月 8 日
Nitroethane
   40
Nitroethane  
   2021 年 9 月 11 日
@tuxz #39 求问这是什么软件生成的图片?
The Go Programming Language
› http://golang.org/
› Go Playground
Go Projects
› Revel Web Framework
© 2026 V2EX · 59ms · 3.9.8.5