先贴代码:
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 所在的作用域?
1
Mohanson 2021-09-07 15:21:54 +08:00
靠作用回收内存的手段叫 RAII (c++, rust), Go 用的是引用计数, 原理不一样.
|
2
gamexg 2021-09-07 15:36:16 +08:00
在
``` if true { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig } ``` 后加个 fmt.Print(data) 试试。 data 没在使用,编译器可能回优化掉了。 |
3
CRVV 2021-09-07 15:40:02 +08:00
> 难道 golang 的 gc 机制就是这样,发现后续没有再使用这个 map 就直接 gc 了,尽管还没有离开这个 map 所在的作用域?
对的,就是这样。 |
4
MoYi123 2021-09-07 16:05:39 +08:00
msg[0] = 0;
改成 for ii, _ := range msg { msg[ii] = 0 //访问一下内存, 触发从内核真正分配内存 } 就是 2G 内存了 我感觉是 msg[0]这样写是只取了一页的内存,所以还有 70MB,要是 map 被 gc 了,应该不会用这多内存的。 |
5
flycloud OP @gamexg 在阻塞代码之后再使用 data,内存肯定不会降低的(已验证)。所以肯定是因为 map 被 gc 了。
但是为什么是 3 分钟后内存才瞬间降低, 然后就一直占有着 70MB,就比较奇怪了。 |
6
flycloud OP @MoYi123 效果是一样的,msg[0] = 0 只访问这一个数据,RES 内存就是 2GB,说明访问了之后就分配了全部的内存,而不是只分配了一页。
|
8
flycloud OP @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 的内存。 |
9
MoYi123 2021-09-07 16:47:08 +08:00
是的,我环境是
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 上也是一样。 |
10
jtgogogo 2021-09-07 16:51:36 +08:00
我这边一直都是 72M
|
11
flycloud OP @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 了导致内存降低了? |
12
jtgogogo 2021-09-07 16:51:57 +08:00
MAC
|
13
PureWhiteWu 2021-09-07 16:55:15 +08:00
@flycloud 3 分钟后才降低是 sysmon 每两分钟执行一次强制 gc 导致的。
|
14
PureWhiteWu 2021-09-07 16:55:38 +08:00
@Mohanson go 不是引用计数,是三色标记法
|
15
flycloud OP |
16
flycloud OP @PureWhiteWu 嗯,可以明确的是 data 被 gc 了,但是剩下的 70MB 是哪儿去了呢
|
17
tuxz 2021-09-07 17:07:48 +08:00
请问这种图是怎么生成的呢
|
18
ksco 2021-09-07 17:11:21 +08:00
不管是什么垃圾回收算法,一定是根据内存是否还被引用来判断是否应该被回收。data 在程序结束前一直保持着对 map 的引用,所以是不会被 GC 的。所以 data 一定不是被 “GC” 了。
我猜测是因为你的程序中只用到了一个大 slice 的一小部分,所以没有用到的部分可能是被 Go 优化器回收了?不过这个就纯属拍脑袋瞎猜了。 |
19
flycloud OP |
21
MrKrabs 2021-09-07 17:18:36 +08:00
编译器优化掉了吧
|
22
flycloud OP 应该是 gc 机制如此。
GODEBUG=madvdontneed=1 go build -gcflags="-N -l" 关闭了编译器优化,内存还是降低了。 |
23
gamexg 2021-09-07 17:33:05 +08:00
@flycloud #5
印象 go 内存回收是有一个独立线程执行的, 按照一定的策略定时执行,策略具体细节记不清,印象是新增内存达到一定比例或达到一定时间。 可以运行 runtime.GC() 来手动触发内存回收,可能需要手动调用多次才能完全释放。 |
24
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 } } |
25
flycloud OP @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 } } |
26
flycloud OP |
27
ksco 2021-09-07 17:58:05 +08:00
你运行一下上面我贴的代码试试呗,看会不会被 GC 。
|
28
flycloud OP |
29
darrh00 2021-09-07 18:29:51 +08:00
|
31
codehz 2021-09-07 18:43:06 +08:00
“访问一下内存, 触发从内核真正分配内存”这个,内核也不知道你数据结构有多大啊。。。
然后为啥一开始会吃 2G 呢,那多半是 go 使用 mmap 的 populate 选项了,这个选项能保证立即分配所有内存,但是不保证之后就不回收回去鸭* |
32
Orlion 2021-09-07 18:51:00 +08:00
进到 swap 了?
|
33
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,这个等待时间不随机不可调。 |
34
ksco 2021-09-07 20:20:41 +08:00
|
35
jeeyong 2021-09-07 20:34:21 +08:00
瞎猜一下, 是不是在把东西写到 swap 里?
|
36
vindurriel 2021-09-08 06:13:14 +08:00 via iPhone
GODEBUG=gctrace=1
|
37
bruce0 2021-09-08 09:15:37 +08:00
猜测一下,会不会是程序一开始就只给 slice 分配了 70M,但是 go 的 runtime 向操作系统申请了 2G 内存,未使用的部分(2G-70M)存在 HeapIdle 区中,因为长时间没有使用,HeapIdle 中的内存又归还给操作系统了
|
38
flycloud OP 破案了,开了 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 // 垃圾回收器最后一次执行时间。 ```` |
39
tuxz 2021-09-08 11:45:32 +08:00
|
40
Nitroethane 2021-09-11 01:50:36 +08:00
@tuxz #39 求问这是什么软件生成的图片?
|