golang 内存回收的疑问

2021-09-07 15:12:46 +08:00
 flycloud

先贴代码:

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 所在的作用域?

3782 次点击
所在节点    Go 编程语言
40 条回复
MrKrabs
2021-09-07 17:18:36 +08:00
编译器优化掉了吧
flycloud
2021-09-07 17:27:16 +08:00
应该是 gc 机制如此。

GODEBUG=madvdontneed=1 go build -gcflags="-N -l"
关闭了编译器优化,内存还是降低了。
gamexg
2021-09-07 17:33:05 +08:00
@flycloud #5
印象 go 内存回收是有一个独立线程执行的,
按照一定的策略定时执行,策略具体细节记不清,印象是新增内存达到一定比例或达到一定时间。
可以运行 runtime.GC() 来手动触发内存回收,可能需要手动调用多次才能完全释放。
ksco
2021-09-07 17:45:20 +08:00
@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
2021-09-07 17:51:42 +08:00
@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
2021-09-07 17:54:15 +08:00
@ksco 看你的 go 版本,不同版本内存回收策略不一样。编译时强制使用 madvdontneed 。

GODEBUG=madvdontneed=1 go build
ksco
2021-09-07 17:58:05 +08:00
你运行一下上面我贴的代码试试呗,看会不会被 GC 。
flycloud
2021-09-07 18:13:14 +08:00
@ksco

centos 下表现一样,过几分钟后 GC,瞬间降低到 70MB 。
mac 下过几分钟后开始内存慢慢下降。
darrh00
2021-09-07 18:29:51 +08:00
@flycloud #26

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

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

为什么刚好是 250s ( 4.1 分钟),是因为 Go 每隔 120s 会触发一次 force GC,这个等待时间不随机不可调。
ksco
2021-09-07 20:20:41 +08:00
@Sasasu @flycloud

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

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

看来变量的 liveness 和 scope 并不一定是一致的。
jeeyong
2021-09-07 20:34:21 +08:00
瞎猜一下, 是不是在把东西写到 swap 里?
vindurriel
2021-09-08 06:13:14 +08:00
GODEBUG=gctrace=1
bruce0
2021-09-08 09:15:37 +08:00
猜测一下,会不会是程序一开始就只给 slice 分配了 70M,但是 go 的 runtime 向操作系统申请了 2G 内存,未使用的部分(2G-70M)存在 HeapIdle 区中,因为长时间没有使用,HeapIdle 中的内存又归还给操作系统了
flycloud
2021-09-08 10:21:22 +08:00
破案了,开了 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
2021-09-08 11:45:32 +08:00
Nitroethane
2021-09-11 01:50:36 +08:00
@tuxz #39 求问这是什么软件生成的图片?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/800407

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX