V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
beego
flycloud
V2EX  ›  Go 编程语言

golang 内存回收的疑问

  •  
  •   flycloud · 83 天前 · 2823 次点击
    这是一个创建于 83 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先贴代码:

    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   83 天前
    靠作用回收内存的手段叫 RAII (c++, rust), Go 用的是引用计数, 原理不一样.
    gamexg
        2
    gamexg   83 天前



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

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

    对的,就是这样。
    MoYi123
        4
    MoYi123   83 天前
    msg[0] = 0;

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

    就是 2G 内存了

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

    但是为什么是 3 分钟后内存才瞬间降低, 然后就一直占有着 70MB,就比较奇怪了。
    flycloud
        6
    flycloud   83 天前
    @MoYi123 效果是一样的,msg[0] = 0 只访问这一个数据,RES 内存就是 2GB,说明访问了之后就分配了全部的内存,而不是只分配了一页。
    MoYi123
        7
    MoYi123   83 天前
    @flycloud 我跑这个代码的表现和你的完全不同。阻塞代码之后再使用 data,我这里也是 70MB.
    flycloud
        8
    flycloud   83 天前
    @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   83 天前
    是的,我环境是
    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   83 天前
    我这边一直都是 72M
    flycloud
        11
    flycloud   83 天前
    @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   83 天前
    MAC
    PureWhiteWu
        13
    PureWhiteWu   83 天前
    @flycloud 3 分钟后才降低是 sysmon 每两分钟执行一次强制 gc 导致的。
    PureWhiteWu
        14
    PureWhiteWu   83 天前
    @Mohanson go 不是引用计数,是三色标记法
    flycloud
        15
    flycloud   83 天前
    @jtgogogo 是的,我在 mac 下运行一直是 70MB 。

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

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

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

    GODEBUG=madvdontneed=1 go build
    ksco
        27
    ksco   83 天前
    你运行一下上面我贴的代码试试呗,看会不会被 GC 。
    flycloud
        28
    flycloud   83 天前
    @ksco

    centos 下表现一样,过几分钟后 GC,瞬间降低到 70MB 。
    mac 下过几分钟后开始内存慢慢下降。
    darrh00
        29
    darrh00   83 天前
    @flycloud #26

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

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

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

    看来变量的 liveness 和 scope 并不一定是一致的。
    jeeyong
        35
    jeeyong   83 天前
    瞎猜一下, 是不是在把东西写到 swap 里?
    vindurriel
        36
    vindurriel   82 天前 via iPhone
    GODEBUG=gctrace=1
    bruce0
        37
    bruce0   82 天前
    猜测一下,会不会是程序一开始就只给 slice 分配了 70M,但是 go 的 runtime 向操作系统申请了 2G 内存,未使用的部分(2G-70M)存在 HeapIdle 区中,因为长时间没有使用,HeapIdle 中的内存又归还给操作系统了
    flycloud
        38
    flycloud   82 天前   ❤️ 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 // 垃圾回收器最后一次执行时间。
    ````
    Nitroethane
        40
    Nitroethane   79 天前
    @tuxz #39 求问这是什么软件生成的图片?
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1572 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 17:16 · PVG 01:16 · LAX 09:16 · JFK 12:16
    ♥ Do have faith in what you're doing.