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-09-07 15:12:46 +08:00 · 3988 次点击
    这是一个创建于 1164 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先贴代码:

    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-09-07 15:21:54 +08:00
    靠作用回收内存的手段叫 RAII (c++, rust), Go 用的是引用计数, 原理不一样.
    gamexg
        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 没在使用,编译器可能回优化掉了。
    CRVV
        3
    CRVV  
       2021-09-07 15:40:02 +08:00
    > 难道 golang 的 gc 机制就是这样,发现后续没有再使用这个 map 就直接 gc 了,尽管还没有离开这个 map 所在的作用域?

    对的,就是这样。
    MoYi123
        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 了,应该不会用这多内存的。
    flycloud
        5
    flycloud  
    OP
       2021-09-07 16:28:49 +08:00
    @gamexg 在阻塞代码之后再使用 data,内存肯定不会降低的(已验证)。所以肯定是因为 map 被 gc 了。

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

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

    我猜测是因为你的程序中只用到了一个大 slice 的一小部分,所以没有用到的部分可能是被 Go 优化器回收了?不过这个就纯属拍脑袋瞎猜了。
    flycloud
        19
    flycloud  
    OP
       2021-09-07 17:14:29 +08:00
    flycloud
        20
    flycloud  
    OP
       2021-09-07 17:15:09 +08:00
    @ksco 不是这样哈,分配出来的 slice,全部遍历了,也是一样的结果。
    MrKrabs
        21
    MrKrabs  
       2021-09-07 17:18:36 +08:00
    编译器优化掉了吧
    flycloud
        22
    flycloud  
    OP
       2021-09-07 17:27:16 +08:00
    应该是 gc 机制如此。

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

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

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

    GODEBUG 是个运行时环境变量,编译时指定只影响 go build 本身,不会影响编译出来的程序。
    flycloud
        30
    flycloud  
    OP
       2021-09-07 18:40:19 +08:00
    @darrh00 感谢指正
    codehz
        31
    codehz  
       2021-09-07 18:43:06 +08:00
    “访问一下内存, 触发从内核真正分配内存”这个,内核也不知道你数据结构有多大啊。。。
    然后为啥一开始会吃 2G 呢,那多半是 go 使用 mmap 的 populate 选项了,这个选项能保证立即分配所有内存,但是不保证之后就不回收回去鸭*
    Orlion
        32
    Orlion  
       2021-09-07 18:51:00 +08:00
    进到 swap 了?
    Sasasu
        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,这个等待时间不随机不可调。
    ksco
        34
    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
        35
    jeeyong  
       2021-09-07 20:34:21 +08:00
    瞎猜一下, 是不是在把东西写到 swap 里?
    vindurriel
        36
    vindurriel  
       2021-09-08 06:13:14 +08:00 via iPhone
    GODEBUG=gctrace=1
    bruce0
        37
    bruce0  
       2021-09-08 09:15:37 +08:00
    猜测一下,会不会是程序一开始就只给 slice 分配了 70M,但是 go 的 runtime 向操作系统申请了 2G 内存,未使用的部分(2G-70M)存在 HeapIdle 区中,因为长时间没有使用,HeapIdle 中的内存又归还给操作系统了
    flycloud
        38
    flycloud  
    OP
       2021-09-08 10:21:22 +08:00   ❤️ 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-09-08 11:45:32 +08:00
    Nitroethane
        40
    Nitroethane  
       2021-09-11 01:50:36 +08:00
    @tuxz #39 求问这是什么软件生成的图片?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2623 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 15:56 · PVG 23:56 · LAX 07:56 · JFK 10:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.