如何分析golang程序的內(nèi)存使用情況

本文同步發(fā)布于:如何分析golang程序的內(nèi)存使用情況 | yoko blog

前言

本篇文章介紹如何分析golang程序的內(nèi)存使用情況。包含以下幾種方法的介紹:

  1. 執(zhí)行前添加系統(tǒng)環(huán)境變量GODEBUG='gctrace=1'來跟蹤打印垃圾回收器信息
  2. 在代碼中使用runtime.ReadMemStats來獲取程序當(dāng)前內(nèi)存的使用情況
  3. 使用pprof工具

注意龄章,本篇文章前后有關(guān)聯(lián)氓轰,需要順序閱讀枷踏。

從十來行的demo開始

package main

import (
    "log"
    "runtime"
    "time"
)

func f() {
    container := make([]int, 8)
    log.Println("> loop.")
    // slice會動態(tài)擴(kuò)容奏赘,用它來做堆內(nèi)存的申請
    for i := 0; i < 32*1000*1000; i++ {
        container = append(container, i)
    }
    log.Println("< loop.")
    // container在f函數(shù)執(zhí)行完畢后不再使用
}

func main() {
    log.Println("start.")
    f()

    log.Println("force gc.")
    runtime.GC() // 調(diào)用強(qiáng)制gc函數(shù)

    log.Println("done.")
    time.Sleep(1 * time.Hour) // 保持程序不退出
}

在我的centos上編譯并運(yùn)行

$go build -o snippet_mem && ./snippet_mem

打印如下信息:

2019/04/06 14:23:16 start.
2019/04/06 14:23:16 > loop.
2019/04/06 14:23:17 < loop.
2019/04/06 14:23:17 force gc.
2019/04/06 14:23:18 done.

使用top命令查看snippet_mem進(jìn)程的內(nèi)存RSS占用為470M。

$top -p $(pidof snippet_mem)

分析:
直觀上來說水孩,這個(gè)程序在f()函數(shù)執(zhí)行完后满葛,切片的內(nèi)存應(yīng)該被釋放径簿,不應(yīng)該占用470M那么大。

下面讓我們使用一些手段來分析程序的內(nèi)存使用情況嘀韧。

GODEBUG中的gctrace

我們在執(zhí)行demo程序之前添加環(huán)境變量GODEBUG='gctrace=1'來跟蹤打印垃圾回收器信息

$go build -o snippet_mem && GODEBUG='gctrace=1' ./snippet_mem

在分析demo程序的輸出信息之前篇亭,先把gctrace輸出信息的格式以及字段的含義放前面,一會我們的分析要基于這部分內(nèi)容锄贷。

gctrace輸出信息的格式以及字段的含義對應(yīng)的官方文檔:https://godoc.org/runtime
我對它做的翻譯如下:

gctrace: 設(shè)置gctrace=1會使得垃圾回收器在每次回收時(shí)匯總所回收內(nèi)存的大小以及耗時(shí)译蒂,
并將這些內(nèi)容匯總成單行內(nèi)容打印到標(biāo)準(zhǔn)錯(cuò)誤輸出中曼月。
這個(gè)單行內(nèi)容的格式以后可能會發(fā)生變化。
目前它的格式:
    gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P
各字段的含義:
    gc #        GC次數(shù)的編號柔昼,每次GC時(shí)遞增
    @#s         距離程序開始執(zhí)行時(shí)的時(shí)間
    #%          GC占用的執(zhí)行時(shí)間百分比
    #+...+#     GC使用的時(shí)間
    #->#-># MB  GC開始哑芹,結(jié)束,以及當(dāng)前活躍堆內(nèi)存的大小捕透,單位M
    # MB goal   全局堆內(nèi)存大小
    # P         使用processor的數(shù)量
如果信息以"(forced)"結(jié)尾聪姿,那么這次GC是被runtime.GC()調(diào)用所觸發(fā)。

如果gctrace設(shè)置了任何大于0的值乙嘀,還會在垃圾回收器將內(nèi)存歸還給系統(tǒng)時(shí)打印一條匯總信息咳燕。
這個(gè)將內(nèi)存歸還給系統(tǒng)的操作叫做scavenging。
這個(gè)匯總信息的格式以后可能會發(fā)生變化乒躺。
目前它的格式:
    scvg#: # MB released  printed only if non-zero
    scvg#: inuse: # idle: # sys: # released: # consumed: # (MB)
各字段的含義:
    scvg#        scavenge次數(shù)的變化,每次scavenge時(shí)遞增
    inuse: #     MB 垃圾回收器中使用的大小
    idle: #      MB 垃圾回收器中空閑等待歸還的大小
    sys: #       MB 垃圾回收器中系統(tǒng)映射內(nèi)存的大小
    released: #  MB 歸還給系統(tǒng)的大小
    consumed: #  MB 從系統(tǒng)申請的大小

打印如下信息:

2019/04/06 14:28:26 start.
2019/04/06 14:28:26 > loop.
gc 1 @0.001s 0%: 0.005+0.92+0.004 ms clock, 0.011+0.027/0/0.13+0.009 ms cpu, 4->6->2 MB, 5 MB goal, 2 P
gc 2 @0.003s 0%: 0.002+0.43+0.002 ms clock, 0.004+0.013/0/0.32+0.005 ms cpu, 5->5->1 MB, 6 MB goal, 2 P
gc 3 @0.003s 1%: 0.002+0.47+0.003 ms clock, 0.004+0.027/0/0.44+0.006 ms cpu, 4->4->2 MB, 5 MB goal, 2 P
gc 4 @0.004s 1%: 0.002+0.50+0.003 ms clock, 0.004+0.022/0/0.48+0.007 ms cpu, 5->5->2 MB, 6 MB goal, 2 P
gc 5 @0.004s 1%: 0.001+1.2+0.003 ms clock, 0.003+0.070/0/1.1+0.006 ms cpu, 6->6->3 MB, 7 MB goal, 2 P
gc 6 @0.006s 1%: 0.002+1.8+0.004 ms clock, 0.004+0.027/0.001/1.8+0.008 ms cpu, 8->8->4 MB, 9 MB goal, 2 P
gc 7 @0.008s 1%: 0.002+2.4+0.005 ms clock, 0.005+0.042/0/2.4+0.010 ms cpu, 10->10->5 MB, 11 MB goal, 2 P
gc 8 @0.010s 1%: 0.002+1.0+0.004 ms clock, 0.005+0.025/0/0.99+0.008 ms cpu, 12->12->6 MB, 13 MB goal, 2 P
gc 9 @0.012s 1%: 0.011+1.8+0.005 ms clock, 0.022+0.025/0/1.7+0.010 ms cpu, 15->15->8 MB, 16 MB goal, 2 P
gc 10 @0.014s 1%: 0.002+3.8+0.004 ms clock, 0.005+0.014/0/3.8+0.009 ms cpu, 19->19->10 MB, 20 MB goal, 2 P
gc 11 @0.018s 1%: 0.003+2.0+0.004 ms clock, 0.006+0.026/0/2.0+0.008 ms cpu, 24->24->13 MB, 25 MB goal, 2 P
gc 12 @0.020s 1%: 0.002+3.0+0.005 ms clock, 0.005+0.028/0/3.0+0.011 ms cpu, 30->30->16 MB, 31 MB goal, 2 P
gc 13 @0.024s 0%: 0.003+9.0+0.004 ms clock, 0.006+0.028/0/9.0+0.009 ms cpu, 38->38->21 MB, 39 MB goal, 2 P
gc 14 @0.033s 0%: 0.002+4.6+0.005 ms clock, 0.005+0.036/0/4.6+0.011 ms cpu, 47->47->26 MB, 48 MB goal, 2 P
gc 15 @0.039s 0%: 0.003+13+0.004 ms clock, 0.007+0.024/0/13+0.009 ms cpu, 59->59->33 MB, 60 MB goal, 2 P
gc 16 @0.053s 0%: 0.002+17+0.005 ms clock, 0.005+0.030/0.027/17+0.011 ms cpu, 74->74->41 MB, 75 MB goal, 2 P
gc 17 @0.072s 0%: 0.049+29+0.004 ms clock, 0.098+0.015/0.091/29+0.009 ms cpu, 93->93->51 MB, 94 MB goal, 2 P
gc 18 @0.103s 0%: 0.003+29+0.005 ms clock, 0.007+0.031/0.029/29+0.010 ms cpu, 116->116->64 MB, 117 MB goal, 2 P
gc 19 @0.134s 0%: 0.003+41+0.004 ms clock, 0.006+0.016/0.030/41+0.009 ms cpu, 145->145->80 MB, 146 MB goal, 2 P
gc 20 @0.178s 0%: 0.003+44+0.005 ms clock, 0.006+0.016/0.045/44+0.010 ms cpu, 181->181->101 MB, 182 MB goal, 2 P
gc 21 @0.223s 0%: 0.003+55+0.004 ms clock, 0.006+0.015/0.044/55+0.008 ms cpu, 227->227->126 MB, 228 MB goal, 2 P
gc 22 @0.281s 0%: 0.004+67+0.004 ms clock, 0.009+0.048/0.023/67+0.008 ms cpu, 284->284->157 MB, 285 MB goal, 2 P
gc 23 @0.352s 0%: 0.004+90+0.005 ms clock, 0.008+0.035/0.042/90+0.011 ms cpu, 355->355->197 MB, 356 MB goal, 2 P
2019/04/06 14:28:27 < loop.
2019/04/06 14:28:27 force gc.
gc 24 @0.446s 0%: 0.005+107+0.007 ms clock, 0.010+0.015/0.050/107+0.014 ms cpu, 444->444->0 MB, 445 MB goal, 2 P (forced)
2019/04/06 14:28:27 done.
gc 25 @0.554s 0%: 0.077+0.071+0.002 ms clock, 0.15+0/0.078/0.036+0.004 ms cpu, 0->0->0 MB, 8 MB goal, 2 P (forced)

這里順便一提低缩,gc的打印信息和demo程序log打印的信息是并行往標(biāo)準(zhǔn)錯(cuò)誤輸出打印的嘉冒,所以可能會亂,上面所貼的打印信息的倒數(shù)第3咆繁、4行是我自己手動重排了讳推,重排前的信息如下:

gc 24 @0.446s 0%: 2019/04/06 14:28:27 force gc.
0.005+107+0.007 ms clock, 0.010+0.015/0.050/107+0.014 ms cpu, 444->444->0 MB, 445 MB goal, 2 P (forced)

demo程序之后會每隔一段時(shí)間打印一些gc信息,匯總?cè)缦拢?/p>

GC forced
gc 26 @120.562s 0%: 0.008+0.18+0.005 ms clock, 0.016+0/0.051/0.10+0.010 ms cpu, 0->0->0 MB, 8 MB goal, 2 P
scvg0: inuse: 0, idle: 959, sys: 959, released: 447, consumed: 512 (MB)
GC forced
gc 27 @240.562s 0%: 0.005+0.19+0.005 ms clock, 0.010+0/0.063/0.13+0.010 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
GC forced
scvg1: 512 MB released
scvg1: inuse: 0, idle: 959, sys: 959, released: 959, consumed: 0 (MB)
gc 28 @360.564s 0%: 0.007+0.099+0.004 ms clock, 0.014+0/0.036/0.13+0.008 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
GC forced
gc 29 @480.565s 0%: 0.006+0.30+0.005 ms clock, 0.013+0/0.048/0.12+0.010 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
scvg2: 0 MB released
scvg2: inuse: 0, idle: 959, sys: 959, released: 959, consumed: 0 (MB)
GC forced
gc 30 @600.566s 0%: 0.004+0.11+0.005 ms clock, 0.009+0/0.045/0.15+0.010 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
scvg3: inuse: 0, idle: 959, sys: 959, released: 959, consumed: 0 (MB)
GC forced
gc 31 @720.566s 0%: 0.004+0.081+0.004 ms clock, 0.009+0/0.024/0.10+0.008 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
GC forced
gc 32 @840.567s 0%: 0.006+0.12+0.005 ms clock, 0.012+0/0.039/0.17+0.010 ms cpu, 0->0->0 MB, 4 MB goal, 2 P
scvg4: inuse: 0, idle: 959, sys: 959, released: 959, consumed: 0 (MB)

分析:

先看在f()函數(shù)執(zhí)行完后立即打印的gc 24那行的信息玩般。444->444->0 MB, 445 MB goal表示垃圾回收器已經(jīng)把444M的內(nèi)存標(biāo)記為非活躍的內(nèi)存银觅。
再看0.1秒之后的gc 250->0->0 MB, 8 MB goal表示垃圾回收器中的全局堆內(nèi)存大小由445M下降為8M坏为。

結(jié)論:在f()函數(shù)執(zhí)行完后究驴,demo程序中的切片容器所申請的堆空間都被垃圾回收器回收了。

但是此時(shí)top顯示內(nèi)存依然占用470M匀伏。

結(jié)論:垃圾回收器回收了應(yīng)用層的內(nèi)存后洒忧,(可能)并不會立即將內(nèi)存歸還給系統(tǒng)。

接下來看scvg相關(guān)的信息够颠。該信息在demo程序每運(yùn)行一段時(shí)間后打印一次熙侍。

scvg0時(shí)consumed為512M。此時(shí)內(nèi)存還沒有歸還給系統(tǒng)履磨。
scvg1時(shí)consumed為0蛉抓,并且scvg1的released=(scvg0 released + scvg0 consumed)。此時(shí)內(nèi)存已歸還給系統(tǒng)剃诅。
我們通過top命令查看巷送,內(nèi)存占用下降為38M。
之后打印的scvg信息不再有變化综苔。

結(jié)論:垃圾回收器在一段時(shí)間后惩系,(可能)會將回收的內(nèi)存歸還給系統(tǒng)位岔。

到這里,我們對GODEBUG中的gctrace的用法已經(jīng)介紹完畢了堡牡。
實(shí)時(shí)上抒抬,我們最前面的疑問也解決了。
但是我們接下來依然會使用另外幾次方法來分析我們的domo程序晤柄。

runtime.ReadMemStats

我們稍微修改一下demo程序擦剑,在一些執(zhí)行流程上以及f()函數(shù)執(zhí)行完后每10秒使用runtime.ReadMemStats獲取內(nèi)存使用情況。

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("> loop.")
    for i := 0; i < 32*1000*1000; i++ {
        container = append(container, i)
        if i == 16*1000*1000 {
            traceMemStats()

        }
    }
    log.Println("< loop.")
}

func main() {
    log.Println("start.")
    traceMemStats()
    f()
    traceMemStats()

    log.Println("force gc.")
    runtime.GC()

    log.Println("done.")
    traceMemStats()

    go func() {
        for {
            traceMemStats()
            time.Sleep(10 * time.Second)
        }
    }()

    time.Sleep(1 * time.Hour)
}

打印如下信息:

2019/04/06 17:37:52 start.
2019/04/06 17:37:52 Alloc:49328(bytes) HeapIdle:66494464(bytes) HeapReleased:0(bytes)
2019/04/06 17:37:52 > loop.
2019/04/06 17:37:52 Alloc:238510080(bytes) HeapIdle:364863488(bytes) HeapReleased:334856192(bytes)
2019/04/06 17:37:52 < loop.
2019/04/06 17:37:52 Alloc:207053496(bytes) HeapIdle:664731648(bytes) HeapReleased:396263424(bytes)
2019/04/06 17:37:52 force gc.
2019/04/06 17:37:52 done.
2019/04/06 17:37:52 Alloc:49864(bytes) HeapIdle:871768064(bytes) HeapReleased:396255232(bytes)
2019/04/06 17:37:52 Alloc:51056(bytes) HeapIdle:871727104(bytes) HeapReleased:396222464(bytes)
// ... 省略部分日志
2019/04/06 17:42:32 Alloc:52304(bytes) HeapIdle:871718912(bytes) HeapReleased:396214272(bytes)
2019/04/06 17:42:42 Alloc:52416(bytes) HeapIdle:871718912(bytes) HeapReleased:396214272(bytes)
2019/04/06 17:42:52 Alloc:52528(bytes) HeapIdle:871718912(bytes) HeapReleased:603217920(bytes)
2019/04/06 17:43:02 Alloc:52640(bytes) HeapIdle:871718912(bytes) HeapReleased:871653376(bytes)
2019/04/06 17:43:12 Alloc:52752(bytes) HeapIdle:871718912(bytes) HeapReleased:871653376(bytes)
2019/04/06 17:43:22 Alloc:52864(bytes) HeapIdle:871718912(bytes) HeapReleased:871653376(bytes)

可以看到芥颈,打印done.之后那條trace信息惠勒,Alloc已經(jīng)下降,即內(nèi)存已被垃圾回收器回收爬坑。在2019/04/06 17:42:522019/04/06 17:43:02的兩條trace信息中纠屋,HeapReleased開始上升,即垃圾回收器把內(nèi)存歸還給系統(tǒng)盾计。距離打印done.時(shí)有5分鐘時(shí)間間隔售担。

另外,MemStats還可以獲取其它哪些信息以及字段的含義可以參見官方文檔:
http://golang.org/pkg/runtime/#MemStats

使用pprof工具

在網(wǎng)頁上查看內(nèi)存使用情況署辉,需在代碼中添加兩行代碼

import(
    "net/http"
    _ "net/http/pprof"
)

go func() {
    log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
}()

然后就可以使用瀏覽器打開以下地址查看內(nèi)存信息 http://127.0.0.1:10000/debug/pprof/heap?debug=1

使用此方法族铆,除了有MemStats的信息,還有申請內(nèi)存發(fā)生在哪些函數(shù)的信息哭尝。

總結(jié)

golang的垃圾回收器在回收了應(yīng)用層的內(nèi)存后哥攘,有可能并不會立即將回收的內(nèi)存歸還給操作系統(tǒng)。

如果我們要觀察應(yīng)用層代碼使用的內(nèi)存大小材鹦,可以觀察Alloc字段逝淹。
如果我們要觀察程序從系統(tǒng)申請的內(nèi)存以及歸還給系統(tǒng)的情況,可以觀察HeapIdleHeapReleased字段侠姑。

以上3種方法创橄,都是獲取了程序的MemStats信息。區(qū)別是:第一種完全不用修改程序莽红,第二種可以在指定位置獲取信息妥畏,第三種可以查看具體哪些函數(shù)申請了內(nèi)存。

結(jié)尾

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末安吁,一起剝皮案震驚了整個(gè)濱河市醉蚁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鬼店,老刑警劉巖网棍,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異妇智,居然都是意外死亡滥玷,警方通過查閱死者的電腦和手機(jī)氏身,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惑畴,“玉大人蛋欣,你說我怎么就攤上這事∪绱” “怎么了陷虎?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長杠袱。 經(jīng)常有香客問我尚猿,道長,這世上最難降的妖魔是什么楣富? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任凿掂,我火速辦了婚禮,結(jié)果婚禮上纹蝴,老公的妹妹穿的比我還像新娘缠劝。我一直安慰自己,他們只是感情好骗灶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秉馏,像睡著了一般耙旦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萝究,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天免都,我揣著相機(jī)與錄音,去河邊找鬼帆竹。 笑死绕娘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的栽连。 我是一名探鬼主播险领,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秒紧!你這毒婦竟也來了绢陌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤熔恢,失蹤者是張志新(化名)和其女友劉穎脐湾,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叙淌,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秤掌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年愁铺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闻鉴。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茵乱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出椒拗,到底是詐尸還是另有隱情似将,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布蚀苛,位于F島的核電站在验,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏堵未。R本人自食惡果不足惜腋舌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望渗蟹。 院中可真熱鬧块饺,春花似錦、人聲如沸雌芽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽世落。三九已至淮腾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屉佳,已是汗流浹背谷朝。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留武花,地道東北人圆凰。 一個(gè)月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像体箕,于是被迫代替她去往敵國和親专钉。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容