分析golang程序内存使用情况
本篇文章简单介绍一下如何分析golang程序的内存使用情况。包含以下几种方法的介绍:
执行前添加系统环境变量
GODEBUG='gctrace=1'
来跟踪打印垃圾回收器信息在代码中使用runtime.ReadMemStats来获取程序当前内存的使用情况
使用pprof工具
一、一个简单的demo代码
package main
import (
"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。
问题:从直观上来说,这个程序在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 从系统申请的大小
打印信息如下:
分析:
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。垃圾回收器回收了应用层的内存后,需要在一段时间内才能将回收的内存归还给系统。
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
字段。如果我们要观察程序从系统申请的内存以及归还给系统的情况,可以观察HeapIdle
和HeapReleased
字段。
以上3种方法,都是获取了程序的MemStats信息。区别是:第一种完全不用修改程序,第二种可以在指定位置获取信息,第三种可以查看具体哪些函数申请了内存。