go 提供了很多的内存指标,多了就容易分不清楚,本文解读几个容易让人迷糊的指标。
内存管理机制
磨刀不误砍柴工,我们先了解下 go 如何管理内存的。
首先,go 是 runtime
自己管理内存的,并没有依赖 libc
这种内存分配器。
具体而言,就是 runtime
自己直接用 mmap
从操作系统申请内存,供自己使用,没有中间商赚差价。
不过,也只是这种中间商角色,go runtime
自己来干了。
因为从操作系统申请内存,都是按页来申请,go 中的内存对象可以是任意大小的。
所以,go runtime
搞了个 mheap 数据结构,来维护所有申请到的内存,还搞了 mcentral
, mcache
, span
好多级来管理。
为了分配不同大小的内存对象,go 分了 67 个级别的 span
,每个级别的 span
对应一个 size,可以理解为这个 size 的内存池。
分配内存的时候,从 size 最小且合适的 span
中分配一个出来。
是的,这种搞法是会浪费一些内存,不过可以比较好的解决,内存碎片的问题。
其他细节这里就先不展开了,简单而言,通常 C 程序用的 libc
中的内存分配器的活,go runtime
也自己干了。
与操作系统交互
当现有的 span
没有空闲的内存块时,go runtime
会通过 mmap
从操作系统申请内存。
但是,有意思的是,go runtime
释放内存的时候,并不是用 munmap
,而是用的 madvise
。
其中区别是,munmap
是将整个地址空间都还给系统,madvise
并不会将地址空间归还,而只是给系统一个建议(advise),说这个地址空间对应的物理内存,go runtime
暂时不用了,系统可以释放了,但是虚拟地址空间还是留着的。
下一次 go runtime
需要的时候,再通过 madvise
让系统为这个地址空间准备物理内存。
这里有个小插曲,go 1.12 版本,将 madvise
的参数,也就是给系统的建议,从默认 MADV_DONTNEED
改成了默认 MADV_FREE
。
比起 MADV_DONTNEED
,MADV_FREE
会更激进一些,操作系统并不会立马释放物理内存,而只是在物理内存比较紧张的时候,再真的释放。
这么改的目的是优化,可以减少下一次需要的时候,会触发的 pagefault
行为。
只是,这个副作用也比较明显的,没有释放的物理内存还是归属于 go 程序,所以 RSS
也算在 go 程序的头上,这个对于指标理解会造成很大的困扰,也更容易造成 OOM
。
所以,后面又改回了默认 MADV_FREE
,具体可见这个 CL:https://go-review.googlesource.com/c/go/+/267100/
内存指标
回到主题,go runtime
提供了很多的内存指标,文档也写得挺清楚的。
https://pkg.go.dev/runtime#MemStats
选几个常用的指标,做些个人直白解读:
HeapReleased
已经释放给系统,只保留了地址空间的内存大小
HeapIdle
处于空闲状态的 span
的内存大小
注意,这里包含了 HeapReleased
。HeapIdle
- HeapReleased
,就是 go runtime
目前预留的空闲内存池,
分配内存的时候,会优先从这里找合适的。
HeapInuse
处于被使用的 span
的内存大小
需要注意的是,如果 span
里有一个在使用的 GC object,整个 span
也处于 inuse
状态。
对象 Inused,包括:真实 alive(被 GC root 引用);新申请还没有被 mark
;mark
标记死亡,但是还没有 sweep
掉。
HeapSys
总共从系统申请的内存地址大小
注意并不是 RSS
,有两种可能使得这个值与 RSS
不一样:
- 申请了地址空间,但是没有读写,系统并不会分配真实物理内存
- 物理内存释放给系统了,但是地址空间还是保留的,也就是
HeapReleased
.
当然,还有不在 heap
管理的 RSS
。
HeapAlloc
GC 管理的对象的大小
注意,是 object 粒度,不是 span
。HeapInuse
- HeapAlloc
,也就是 Inused span
中还剩余的空间内存块。
这个语义跟 Inused 类似,除了包含活对象,还包括 mark
标记死亡,但是还没 sweep
调用的。
不过,如果 sweep
掉了,也就不算在内了,即使内存还没有还给系统。
StackInuse
被当做 stack
正在使用的 span
的大小
没有 StackIdle,因为 stack
和 heap
,其实都是通过 mheap
那一套机制管理的,Idle 的 span
,都算在 HeapIdle
里面了。
总结
go 舍弃了 libc
这类内存分配管理的中间商,自己重新撸一套,确实可以看有一些新花样出来(不只提到的这些,还有很多新花样绑定在 go 自己的内存分配管理实现上了),并不是简单的重复造轮子。
至于指标的含义,确实是指标太多了,每个都记住了估计是很难的。
个人的做法,理解清楚原理,搞清楚几个常用的,没记住的,用的时候再翻文档吧。