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

golang 内存回收的疑问

 
  •   flycloud 2021 年 9 月 7 日 4627 次点击
    这是一个创建于 1586 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先贴代码:

    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 求问这是什么软件生成的图片?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2682 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 11:32 PVG 19:32 LAX 03:32 JFK 06:32
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86