Golang Go语言内存回收的疑问
Golang Go语言内存回收的疑问
先贴代码:
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 所在的作用域?
更多关于Golang Go语言内存回收的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
靠作用回收内存的手段叫 RAII (c++, rust), Go 用的是引用计数, 原理不一样.
更多关于Golang Go语言内存回收的疑问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
在<br> if true {<br> sig := make(chan os.Signal, 1)<br> signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)<br> <-sig<br> }<br>
后加个 fmt.Print(data) 试试。
data 没在使用,编译器可能回优化掉了。
> 难道 golang 的 gc 机制就是这样,发现后续没有再使用这个 map 就直接 gc 了,尽管还没有离开这个 map 所在的作用域?
对的,就是这样。
msg[0] = 0;
改成
for ii, _ := range msg {
msg[ii] = 0 //访问一下内存, 触发从内核真正分配内存
}
就是 2G 内存了
我感觉是 msg[0]这样写是只取了一页的内存,所以还有 70MB,要是 map 被 gc 了,应该不会用这多内存的。
在阻塞代码之后再使用 data,内存肯定不会降低的(已验证)。所以肯定是因为 map 被 gc 了。
但是为什么是 3 分钟后内存才瞬间降低, 然后就一直占有着 70MB,就比较奇怪了。
效果是一样的,msg[0] = 0 只访问这一个数据,RES 内存就是 2GB,说明访问了之后就分配了全部的内存,而不是只分配了一页。
我跑这个代码的表现和你的完全不同。阻塞代码之后再使用 data,我这里也是 70MB.
代码确定是这样的么:<br>func main() {<br> data := make(map[int32][]int32)<br> for i := 0; i < 1024; i++ {<br> msg := make([]int32, 1024 * 512, 1024 * 512)<br> msg[0] = 0 //访问一下内存, 触发从内核真正分配内存<br> data[int32(i)] = msg<br> }<br> fmt.Println(len(data))<br><br> if true {<br> sig := make(chan os.Signal, 1)<br> signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)<br> <-sig<br> }<br> fmt.Println(len(data))<br>}<br>
一直阻塞着,我等了 10 分钟,还是 2GB 的内存。
是的,我环境是
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 上也是一样。
我这边一直都是 72M
我的是:
go version go1.17 darwin/amd64
运行环境是:centos,Linux 172-20-245-36 4.18.0-193.28.1.el8_2.x86_64
你的运行结果更神奇了啊,阻塞后还会使用 data 的被 gc 了导致内存降低了?
MAC
3 分钟后才降低是 sysmon 每两分钟执行一次强制 gc 导致的。
go 不是引用计数,是三色标记法
是的,我在 mac 下运行一直是 70MB 。
但是在 centos 下,编译运行,刚开始是 2GB,几分钟后降低为 70MB 。
请问这种图是怎么生成的呢
不管是什么垃圾回收算法,一定是根据内存是否还被引用来判断是否应该被回收。data 在程序结束前一直保持着对 map 的引用,所以是不会被 GC 的。所以 data 一定不是被 “GC” 了。
我猜测是因为你的程序中只用到了一个大 slice 的一小部分,所以没有用到的部分可能是被 Go 优化器回收了?不过这个就纯属拍脑袋瞎猜了。
不是这样哈,分配出来的 slice,全部遍历了,也是一样的结果。
编译器优化掉了吧
应该是 gc 机制如此。
GODEBUG=madvdontneed=1 go build -gcflags="-N -l"
关闭了编译器优化,内存还是降低了。
#5
印象 go 内存回收是有一个独立线程执行的,
按照一定的策略定时执行,策略具体细节记不清,印象是新增内存达到一定比例或达到一定时间。
可以运行 runtime.GC() 来手动触发内存回收,可能需要手动调用多次才能完全释放。
> 应该是 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
}
}
真大佬,确实啊,调用 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
}
}
看你的 go 版本,不同版本内存回收策略不一样。编译时强制使用 madvdontneed 。
GODEBUG=madvdontneed=1 go build
你运行一下上面我贴的代码试试呗,看会不会被 GC 。
centos 下表现一样,过几分钟后 GC,瞬间降低到 70MB 。
mac 下过几分钟后开始内存慢慢下降。
#26
GODEBUG 是个运行时环境变量,编译时指定只影响 go build 本身,不会影响编译出来的程序。
感谢指正
“访问一下内存, 触发从内核真正分配内存”这个,内核也不知道你数据结构有多大啊。。。
然后为啥一开始会吃 2G 呢,那多半是 go 使用 mmap 的 populate 选项了,这个选项能保证立即分配所有内存,但是不保证之后就不回收回去鸭*
进到 swap 了?
https://i.loli.net/2021/09/07/VgxG4OJyUjY6pDa.jpg
GC trace 显示在第二次 force GC 之后 Go 编译器预知未来了,直接把你的数据干掉了。
我认为是某种 UB,Go 判定的的函数已经返回了。
为什么刚好是 250s ( 4.1 分钟),是因为 Go 每隔 120s 会触发一次 force GC,这个等待时间不随机不可调。
是,编译器确实是在编译阶段就分析(预测)出了 data 不会再被使用了,然后直接 GC 掉了。
使用楼主主贴中给出的程序,运行 go build -gcflags ‘-live’,可以看到编译器知道在 fmt.Println(len(data)) 这行之后 data 再也不会被使用了。
看来变量的 liveness 和 scope 并不一定是一致的。
瞎猜一下, 是不是在把东西写到 swap 里?
GODEBUG=gctrace=1
破案了,开了 pprof,可以看到各项内存占用情况。剩下的那 70MB 是垃圾回收标记元信息使用的内存:GCSys 。
/doge
data gc 前:<br>heap profile: 286: 599785472 [286: 599785472] @ heap/1048576<br>286: 599785472 [286: 599785472] @ 0x696898 0x43bdf6 0x4726e1<br># 0x696897 main.main+0xf7 /data/gowork/src/test/test.go:21<br># 0x43bdf5 runtime.main+0x255 /usr/local/go/src/runtime/proc.go:225<br><br><br># runtime.MemStats<br># Alloc = 2147822008<br># TotalAlloc = 2148011064<br># Sys = 2291062280<br># Lookups = 0<br># Mallocs = 2741<br># Frees = 671<br># HeapAlloc = 2147822008<br># HeapSys = 2214199296<br># HeapIdle = 65634304<br># HeapInuse = 2148564992<br># HeapReleased = 65150976<br># HeapObjects = 2070<br># Stack = 393216 / 393216<br># MSpan = 176528 / 180224<br># MCache = 4800 / 16384<br># BuckHashSys = 1444089<br># GCSys = 73675560<br># OtherSys = 1153511<br># NextGC = 2403760736<br># LastGC = 1631066553972785432<br>
data gc 后:<br>heap profile: 0: 0 [1007: 2111832064] @ heap/1048576<br>0: 0 [1007: 2111832064] @ 0x696898 0x43bdf6 0x4726e1<br># 0x696897 main.main+0xf7 /data/gowork/src/test/test.go:21<br># 0x43bdf5 runtime.main+0x255 /usr/local/go/src/runtime/proc.go:225<br><br><br># runtime.MemStats<br># Alloc = 211840<br># TotalAlloc = 2148063184<br># Sys = 2291062280<br># Lookups = 0<br># Mallocs = 2895<br># Frees = 2059<br># HeapAlloc = 211840<br># HeapSys = 2214133760<br># HeapIdle = 2213347328<br># HeapInuse = 786432<br># HeapReleased = 2213289984<br># HeapObjects = 836<br># Stack = 458752 / 458752<br># MSpan = 45288 / 180224<br># MCache = 4800 / 16384<br># BuckHashSys = 1444089<br># GCSys = 73694016<br># OtherSys = 1135055<br># NextGC = 4194304<br># LastGC = 1631066839647887815<br>
各项指标含义:
```
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 求问这是什么软件生成的图片?
针对Golang(Go语言)内存回收的疑问,以下是一些专业解答:
Go语言采用了一种高效的垃圾回收机制来管理内存。这种机制会自动回收不再使用的内存对象,从而避免了内存泄漏和其他内存管理问题。
Go语言的垃圾回收器主要基于标记-清除算法。该算法会标记所有可以访问的对象为活跃对象,然后清除所有未标记的对象。这种算法能够处理大量的对象,并且能够处理循环引用问题。为了优化性能,Go语言的垃圾回收器还采用了并发标记技术,可以在标记阶段与用户程序并发执行,从而减少停顿时间。
此外,Go语言还引入了三色标记算法来解决标记-清除算法中的内存碎片化问题。在三色标记算法中,垃圾回收器会将所有对象标记为白色、灰色或黑色,以确保在清除阶段能够准确地识别出垃圾对象。
尽管Go语言的垃圾回收机制是自动的,但它仍然会对程序的性能产生一定的影响。因此,开发者在编写Go程序时,需要注意避免频繁地分配内存,以减少垃圾回收器的压力。可以使用复用对象、指针、sync.Pool内存池等技术来优化内存使用,提高程序的性能。
总的来说,Go语言的内存回收机制是一种高效、自动化的内存管理技术,能够确保程序的内存安全,提高程序的健壮性和可靠性。