平时在开发中,有时间需要通过查看内存使用情况来分析程序的性能问题,经常会使用到 MemStats 这个结构体。但平时用到的都是一些最基本的方法,今天我们全面认识一下 MemStas。

相关文件为 src/runtime/mstats.go ,本文章里主要是与内存统计相关。

MemStats 结构体

  1. // MemStats记录有关内存分配器的统计信息
  2. type MemStats struct {
  3. // General statistics.
  4. Alloc uint64
  5. TotalAlloc uint64
  6. Sys uint64
  7. Lookups uint64
  8. Mallocs uint64
  9. Frees uint64
  10. // Heap memory statistics.
  11. HeapAlloc uint64
  12. HeapSys uint64
  13. HeapIdle uint64
  14. HeapInuse uint64
  15. HeapReleased uint64
  16. HeapObjects uint64
  17. // Stack memory statistics.
  18. StackInuse uint64
  19. StackSys uint64
  20. // Off-heap memory statistics.
  21. MSpanInuse uint64
  22. MSpanSys uint64
  23. MCacheInuse uint64
  24. MCacheSys uint64
  25. BuckHashSys uint64
  26. GCSys uint64
  27. OtherSys uint64
  28. // Garbage collector statistics.
  29. NextGC uint64
  30. LastGC uint64
  31. PauseTotalNs uint64
  32. PauseNs \[256\]uint64
  33. PauseEnd \[256\]uint64
  34. NumGC uint32
  35. NumForcedGC uint32
  36. GCCPUFraction float64
  37. EnableGC bool
  38. DebugGC bool
  39. // BySize reports per-size class allocation statistics.
  40. BySize \[61\]struct {
  41. Size uint32
  42. Mallocs uint64
  43. Frees uint64
  44. }
  45. }

可以清楚的看到,统计信息共分了五类

  • 常规统计信息(General statistics)
  • 分配堆内存统计(Heap memory statistics)
  • 栈内存统计(Stack memory statistics)
  • 堆外内存统计信息(Off-heap memory statistics)
  • 垃圾回收器统计信息(Garbage collector statistics)
  • 按 per-size class 大小分配统计(BySize reports per-size class allocation statistics)

以下按分类对每一个字段进行一些说明,尽量对每一个字段的用处可以联想到日常我们工作中用到的一些方法。

常规统计信息(General statistics)

  • Alloc 已分配但尚未释放的字节
  • TotalAlloc 已分配(就算释放也不会减少)
  • Sys 系统中获取的字节 (xxx_sys 的统计, 无锁,近似值)
  • Nlookup runtime 执行时的指针查找数(主要在调试 runtime 内部使用)
  • Nmalloc 分配堆对象的累计数量,活动对象的数量是 Mallocs-Frees
  • Nfree fress Frees 是释放的堆对象的累计计数

分配堆内存统计(Heap memory statistics)

原子更新或 STW

HeapAlloc 已分配但尚未释放的字节 (同上面的alloc一样)
HeapSys 从 os 为堆申请的内存大小
HeapIdle 空闲 spans 字节
HeapInuse 使用中的最大值
HeapReleased 操作系统的物理内存大小
HeapObjects 分配的堆对象总数量

栈内存统计(Stack memory statistics)

stack 不是 heap 的一部分,但 runtime 可以将 heap 内存的一部分用于 stack,反之一样

StackInuse 在 stack span 的字节
StackSys 从 os 中获取的 stack 内存

堆外内存统计信息(Off-heap memory statistics)

MSpanInuse 分配的 mspan 结构的字节
MSpanSys 从 os 中获取的用于 mspan 结构的字节
MCacheInuse 已分配的 mcache 结构的字节
MCacheSys 从 os 中分配的 mcache 结构的字节
BuckHashSys 分析 bucket 哈希表中的内存字节
GCSys GC 中元数据的字节
OtherSys 其它堆外 runtime 分配的字节

垃圾回收器统计信息(Garbage collector statistics)

NextGC 下次 GC 目标堆的大小
LastGC 上次 GC 完成的时间, UNIX 时间戳
PauseTotalNs 从程序开始时累计暂停时长 (STW), 单位纳秒
PauseNs 最近一次的 STW 时间缓存区,最近一次暂停是在 PauseNs[(NumGC+255)%256],通常它是用来记录最近 N%256 次的 GC 记录。
PauseEnd 最近 GC 暂停的缓冲区,缓冲区的存放方式与 PauseNs 一样。每个 GC 有多个暂停,记录最后一次暂停
NumGC 完成的 GC 数量
NumForcedGC 记录应用通过调用 GC 函数强制 GC 的次数
GCCPUFraction 自程序启动后 GC 使用 CPU 时间的分值,其值为 0-1 之间,0 表示 gc 没有消耗当前程序的 CPU。(不包含写屏障的 cpu 时间)
EnableGC 启用 GC, 值为 true,除非使用 GOGC=off 设置
DebugGC 当前未使用

可以看到这些字段的信息还是比较常见的,在我们分析一个程序 GC 的时候,经常会用到 GDEBUG=gctrace=1 go run main.go 这个命令,它的输出不正是这几个字段的信息的么?

按 per-size class 大小分配统计(BySize reports per-size class allocation statistics)

目前对块概念还有些不清楚,待后期补充。

上面是对每个 MemStats 结构体字段的介绍,在 mstats.go 文件中还有一个 mstat,字段与基本与 MemStats 一致,只是为非导出结构体,一定要保证两者的一致性,这点文件中已做了说明。

对 memstat 全局对象的操作一般是在申请或释放内存的时候发生的,如 [memcache.allocLarge()](https://github.com/golang/go/blob/go1.16.2/src/runtime/mcache.go#L208-L249)[memcache.releaseAll()](https://github.com/golang/go/blob/go1.16.2/src/runtime/mcache.go#L251-L290)

下面我们介绍一下与基有关的几个方法

使用方法

MemStats 有关的函数只有 ReadMemStats 这一个。

  1. // ReadMemStats populates m with memory allocator statistics.
  2. //
  3. // The returned memory allocator statistics are up to date as of the
  4. // call to ReadMemStats. This is in contrast with a heap profile,
  5. // which is a snapshot as of the most recently completed garbage
  6. // collection cycle.
  7. func ReadMemStats(m \*MemStats) {
  8. stopTheWorld("read mem stats")
  9. systemstack(func() {
  10. readmemstats\_m(m)
  11. })
  12. startTheWorld()
  13. }

ReadMemStats 使用内存分配器统计信息填充 m。

通过调用 ReadMemStats 返回内存分配器统计最新的信息, 与堆相反,后者是新完成 GC 周期的快照。

可以看到此函数会调用 [stopTheWorld()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L885-L918) 函数,也就是在收集数据期间会一直处于 stop the world 状态,待收集完成后,再调用 [startTheWorld()](https://github.com/golang/go/blob/go1.15.6/src/runtime/proc.go#L920-L927) 恢复状态。

看一个例子

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. )
  6. func main() {
  7. v := struct{}{}
  8. a := make(map\[int\]struct{})
  9. for i := 0; i < 10000; i++ {
  10. a\[i\] = v
  11. }
  12. runtime.GC()
  13. printMemStats("After Map Add 100000")
  14. }
  15. func printMemStats(mag string) {
  16. var m runtime.MemStats
  17. runtime.ReadMemStats(&m)
  18. fmt.Printf("%v:memory = %vKB, GC Times = %vn", mag, m.Alloc/1024, m.NumGC)
  19. }

https://blog.haohtml.com/archives/21685