背景

在一个夜黑风高的晚上,我们的Go重新内存占用RES到4.5G,但是我们的pprof看到确实2.2G。我们的Go版本是1.17,内存释放策略已经是MADV_DONTNEED,那么为什么go进程的内存没有返回给操作系统?

  • 实际内存只有2.2G

  • 在这里插入图片描述

  • 操作系统看到的却是用了4.5G

  • 在这里插入图片描述

  • 在接下来的时间里面,RES指标在缓慢下降
    在这里插入图片描述
    所以,我们怀疑是Go还是占有一部分内存,没有完全归还给操作系统,但是,我们还是需要验证一下

本地复现

在本地复现之前,我们想先看下Go的内存释放策略 MADV_DONTNEED 和 MADV_FREE 有什么区别。于是我选用了两个版本:Go1.15(MADV_FREE) 和 Go1.17(MADV_DONTNEED) 来做实验

复现的代码程序为:https://github.com/wolfogre/go-pprof-practice

Go 1.15

go tool pprof http://localhost:6060/debug/pprof/heap,Go的 pprof 默认使用 -inuse_space,这是Go内存中正在使用的内存大小
在这里插入图片描述在这里插入图片描述

可以发现,在默认的情况下,使用 inuse_space 会出现pprof 和 操作系统看到的内存使用不一致的问题。pprof 看到的内存占用,其实只是 golang 逻辑上正在使用的内存量,不包括已被 GC 回收但尚未返还给操作系统的内存,同样也不包括内核态的内存占用。而 htop 是站在操作系统层面看到的进程内存占用,理论上就是会比 pprof 看到的内存占用量更多的。

结论

  • Go 1.15 因为使用了 MADV_FREE 内存释放方式,RES并不会快速减小。这也符合 MADV_FREE 内存释放方式的特点

Go 1.17

在这里插入图片描述

在这里插入图片描述

在Go 1.17 中我们也可以看到,实际内存要比

当我们过一会再来看的时候,内存已经降到1G多了,我们使用的也是1G,说明一些内存依旧被归还给了操作系统,

在这里插入图片描述
在这里插入图片描述

结论

Go 1.17 因为使用了 MADV_DONTNEED 内存释放方式,RES 会逐渐

为什么会这样

MADV_DONTNEED 和 MADV_FREE

Go 使用 madvise 系统调用来释放物理内存给操作系统,该方法主要有两种归还类型可选:

  • MADV_DONTNEED:立即归还物理内存给操作系统,如果下次访问到该范围的内存,则会触发 page fault 异常,需要重新分配物理页,使用该类型可以减少程序的RES占用
  • MADV_FREE:告诉操作系统这块内存已经不需要使用了,可以回收了,如果内存紧张,操作系统就会将其回收。这实际是一个lazily的释放过程。如果再次访问这块内存的时候,操作系统还没有将其回收,是不会触发 page fault 的。使用该类型,可能程序的RES不会减少。

Go 1.15

使用MADV_FREE方式,程序内存不会立刻回收,即RES值不会立刻下降,只有当OS内存紧缺时才会回收Go程序的内存返回给OS;而Go 1.11以及之前的版本默认采用的是 MADV_DONTNEED方式,程序RES值下降很快。因此如果需要使程序内存占用下降很快的话,可设置环境变量GODEBUG=madvdontneed=1。在 GO1.12~GO1.15 且内核版本 4.5 以上 RES 指标已经无法准确观测服务内存占用;

Go 1.17

默认使用 MADV_DONTNEED,而不是在操作系统处于内存压力下时懒惰地释放内存(使用 MADV_FREE)。 这意味着像 RES 这样的进程级内存统计数据将更准确地反映 Go 进程正在使用的物理内存量。

Go 内存分配策略

  • 每次向操作系统化申请分配一大块内存,由Go来做内存分配,减少系统调用
  • 回收对象时,并不会将内存直接返回给操作系统,而是先放回预先分配的大块内存中,以便复用。所以Go程序的内存即使被GC回收了,也不会立刻归还给操作的,除非你手动调用FreeOSMemory(),

Go内存回收策略

  • 服务启动时,会在runtime的main函数启动一个goroutine,定时检测是否触发归还内存给OS的操作。
    在这里插入图片描述
    在这里插入图片描述

  • 在heap进行扩容时,计算当前持有的内存和即将扩容的size之和是否达到清理阈值,然后会触发归还内存给OS的操作。(mheap.grow→pageAlloc.scavenge)

  • 在这里插入图片描述

  • 手动调用debug.FreeOSMemory()方法,会触发归还内存给OS的操作。

Go1.17默认是使用 MADV_DONTNEED ,那为什么程序的RES并没有减少呢?是因为GC在回收对象时,并不会将内存直接返回给操作系统,而是先放回预先分配的大块内存中,以便复用。所以Go程序的内存即使被GC回收了,也不会立刻归还给操作的。

想准确知道怎么做 结论
  • 我们使用pprof看到的内存使用默认是正在使用的内存
  • 我们线上使用的是Go1.17,默认是使用 MADV_DONTNEED ,那为什么程序的RES并没有减少呢?
    是因为GC在回收对象时,并不会将内存直接返回给操作系统,而是先放回预先分配的大块内存中,以便复用。所以Go程序的内存即使被GC回收了,也不会立刻归还给操作的。
  • 在后续的时间中,RES变小的原因是。Go内存在逐渐向操作系统归还不使用的内存
    使用MADV_DONTNEED和MADV_FREE的区别就是,在MADV_FREE模式中,RES 不会有明显的变化。MADV_DONTNEED RES 可以看到变化,这就是为什么在后面的时间中,RES在慢慢减少。