vlambda博客
学习文章列表

分析golang程序内存使用情况

本篇文章简单介绍一下如何分析golang程序的内存使用情况。包含以下几种方法的介绍:

  1. 执行前添加系统环境变量GODEBUG='gctrace=1'来跟踪打印垃圾回收器信息

  2. 在代码中使用runtime.ReadMemStats来获取程序当前内存的使用情况

  3. 使用pprof工具

一、一个简单的demo代码

package mainimport ( "log" "runtime" "time") func f() { container := make([]int, 8) log.Println("循环申请内存...") for i := 0; i < 32*1000*1000; i++ { container = append(container, i) } log.Println("内存申请结束")} func main() { log.Println("start") f() log.Println("执行GC") runtime.GC() // 调用强制gc函数 log.Println("完成") time.Sleep(1 * time.Hour) // 保持程序不退出}

编译并运行

➜ go build -o demo &&  ./demo

打印信息如下:

查看demo程序占用内存,此时占用内存为597M。

分析golang程序内存使用情况


问题:从直观上来说,这个程序在f()函数执行完后,切片的内存应该被释放,不应该占用597M那么大。


下面让我们使用一些手段来分析程序的内存使用情况。

二、分析方法介绍

1、GODEBUG中的gctrace

我们在执行demo程序之前添加环境变量GODEBUG='gctrace=1'来跟踪打印垃圾回收器信息


➜ go build -o demo && GODEBUG='gctrace=1' ./demo


先简单说明一下gctrace输出信息的格式以及字段的含义,附上官方文档地址。

https://godoc.org/runtime

gctrace: 设置gctrace=1会使得垃圾回收器在每次回收时汇总所回收内存的大小以及耗时,并将这些内容汇总成单行内容打印到标准错误输出中。这个单行内容的格式以后可能会发生变化。目前它的格式: gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P 
各字段的含义: gc # GC次数的编号,每次GC时递增 @#s 距离程序开始执行时的时间 #% GC占用的执行时间百分比 #+...+# GC使用的时间 #->#-># MB GC开始,结束,以及当前活跃堆内存的大小,单位M # MB goal 全局堆内存大小 # P 使用processor的数量如果信息以"(forced)"结尾,那么这次GC是被runtime.GC()调用所触发。
如果gctrace设置了任何大于0的值,还会在垃圾回收器将内存归还给系统时打印一条汇总信息。这个将内存归还给系统的操作叫做scavenging。这个汇总信息的格式以后可能会发生变化。目前它的格式: scvg#: # MB released printed only if non-zero scvg#: inuse: # idle: # sys: # released: # consumed: # (MB)各字段的含义: scvg# scavenge次数的变化,每次scavenge时递增 inuse: # MB 垃圾回收器中使用的大小 idle: # MB 垃圾回收器中空闲等待归还的大小 sys: # MB 垃圾回收器中系统映射内存的大小 released: # MB 归还给系统的大小 consumed: # MB 从系统申请的大小

打印信息如下:

分析golang程序内存使用情况

分析:

        1、先看在f()函数执行完后立即打印的gc 16那行的信息。728->728->0 MB, 729 MB goal表示垃圾回收器已经把728M的内存标记为非活跃的内存。

再看之后的gc 17那行0->0->0 MB, 4 MB goal表示垃圾回收器中的全局堆内存大小由729M下降为4M。

        2、demo程序之后会每隔一段时间打印一些gc信息

结论:在f()函数执行完后,demo程序中的切片容器所申请的堆空间都被垃圾回收器回收了。


但是此时发现demo程序内存占用还有5百多M。垃圾回收器回收了应用层的内存后,需要在一段时间内才能将回收的内存归还给系统。


分析golang程序内存使用情况


2、runtime.ReadMemStats

我们稍微修改一下demo程序,在一些执行流程上以及f()函数执行完后每10秒使用runtime.ReadMemStats获取内存使用情况。

package main import ( "log" "runtime" "time") func traceMemStats() { var ms runtime.MemStats runtime.ReadMemStats(&ms) log.Printf("Alloc:%d(bytes) HeapIdle:%d(bytes) HeapReleased:%d(bytes)", ms.Alloc, ms.HeapIdle, ms.HeapReleased)} func f() { container := make([]int, 8) log.Println("循环申请内存...") for i := 0; i < 32*1000*1000; i++ { container = append(container, i) if i == 16*1000*1000 { traceMemStats() } } log.Println("内存申请结束")} func main() { log.Println("start") traceMemStats() f() traceMemStats()  log.Println("执行GC") runtime.GC() // 调用强制gc函数 traceMemStats()  log.Println("完成") traceMemStats()  go func() { for { traceMemStats() time.Sleep(10 * time.Second) } }()  time.Sleep(1 * time.Hour) // 保持程序不退出}


打印信息如下:



可以看到,Alloc内存会下降,即内存已被垃圾回收器回收。HeapReleased开始上升,即垃圾回收器逐渐在把内存归还给系统

另外,MemStats还可以获取其它哪些信息以及字段的含义可以参见官方文档

https://pkg.go.dev/runtime#MemStats

3、使用pprof工具

在网页上查看内存使用情况,需在代码中添加两行代码


package main import ( "log" "net/http" _ "net/http/pprof" "runtime" "time") func f() { container := make([]int, 8) log.Println("循环申请内存...") for i := 0; i < 32*1000*1000; i++ { container = append(container, i) } log.Println("内存申请结束")} func main() { go func() { log.Println(http.ListenAndServe("0.0.0.0:10000", nil)) }() log.Println("start") f() log.Println("执行GC") runtime.GC() // 调用强制gc函数  log.Println("完成")  time.Sleep(1 * time.Hour) // 保持程序不退出}

http://127.0.0.1:10000/debug/pprof/heap?debug=1

输出信息如下:


三、总结

1、golang的垃圾回收器在回收了应用层的内存后,有可能并不会立即将回收的内存归还给操作系统。

2、如果我们要观察应用层代码使用的内存大小,可以观察Alloc字段。如果我们要观察程序从系统申请的内存以及归还给系统的情况,可以观察HeapIdleHeapReleased字段。

以上3种方法,都是获取了程序的MemStats信息。区别是:第一种完全不用修改程序,第二种可以在指定位置获取信息,第三种可以查看具体哪些函数申请了内存。