最近的一个golang项目上生产后,发现RES内存占用一直在轻微上涨:三天时间从一开始的40M,上涨到了60M左右。之前对golang的内存管理和垃圾回收机制一直不是很清晰,于是花些时间去把这个问题搞清楚。

1) VSZ,RSS,TTY,STAT, VIRT,RES,SHR,DATA的含义

我们需要关注的主要是RES内存

o: VIRT -- Virtual Image (kb)
      The  total  amount  of  virtual  memory  used  by the task.  It
      includes all code, data and shared libraries  plus  pages  that
      have  been  swapped out and pages that have been mapped but not
      used.
 p: SWAP  --  Swapped size (kb)
      Memory that is not resident but is present in a task.  This  is
      memory  that  has been swapped out but could include additional
      non-resident memory.  This column is calculated by  subtracting
      physical memory from virtual memory.
  q: RES  --  Resident size (kb)
      The non-swapped physical memory a task has used.
  r: CODE  --  Code size (kb)
      The  amount  of virtual memory devoted to executable code, also
      known as the 'text resident set' size or TRS.
  s: DATA  --  Data+Stack size (kb)
      The amount of virtual memory devoted to other  than  executable
      code, also known as the 'data resident set' size or DRS.
  t: SHR  --  Shared Mem size (kb)
      The amount of shared memory used by a task.  It simply reflects
      memory that could be potentially shared with other processes.

VIRT:virtual memory usage 虚拟内存
1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等等。
2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量

SHR:shared memory 共享内存
1、除了自身进程的共享内存,也包括其他进程的共享内存
2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
3、计算某个进程所占的物理内存大小公式:RES – SHR
4、swap out后,它将会降下来

RES:resident memory usage 常驻内存
1、进程当前使用的内存大小,但不包括swap out
2、包含其他进程的共享
3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
4、关于库占用内存的情况,它只统计加载的库文件所占内存大小

DATA
1、数据占用的内存。如果top没有显示,按f键可以显示出来。
2、真正的该程序要求的数据空间,是真正在运行中要使用的。

2) golang监控内存的方式

1 利用GCVIS进行可视化监控

原理可参考:http://holys.im/2016/07/01/monitor-golang-gc/
这是最简单的方式,好处是不用修改程序代码。

1)安装

go get https://github.com/davecheney/gcvis 或者git clone https://github.com/davecheney/gcvis 到本地自行安装

2)启动监控(要运行的文件是/path/to/binary)

有两种方式
1、 直接运行

./gcvis /path/to/binary

2、 管道重定向方式(standard error)

GODEBUG=gctrace=1  /path/to/binary  |& ./gcvis

我这里运行的是

env GOMAXPROCS=4 ./gcvis -o=false -p 12345  -i 10.107.101.1 /path/to/binary

可访问页面http://10.107.101.1:12345 获取可视化结果。

2 利用pprof进行监控

具体可参考:https://studygolang.com/articles/9940
go中有pprof包来做代码的性能监控主要涉及两个pkg:

#web服务器:
import (
    "net/http"
    _ "net/http/pprof"
)

#一般应用程序(实际应用无web交互)
import (
    "net/http"
    _ "runtime/pprof"
)

其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来

1)修改代码

需要对原来的程序代码进行修改

web 服务器

如果你的go程序是用http包启动的web服务器,可以选择net/http/pprof。
只需要引入包_"net/http/pprof",然后就可以在浏览器中使用 http://localhost:port/debug/pprof/
直接看到当前web服务的状态,包括CPU占用情况和内存使用情况等。

服务进程

如果你的go程序不是web服务器,而是一个服务进程,也可以选择使用net/http/pprof包,同样引入包net/http/pprof,然后在开启另外一个goroutine来开启端口监听。

go func() {
        http.ListenAndServe("localhost:6060", nil)
}()

应用程序

如果你的go程序只是一个应用程序,比如计算fabonacci数列,那么你就不能使用net/http/pprof包了,你就需要使用到runtime/pprof。
具体做法就是用到pprof.StartCPUProfile和pprof.StopCPUProfile。
比如下面的例子:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }

运行程序的时候加一个--cpuprofile参数,比如 fabonacci --cpuprofile=fabonacci.prof
这样程序运行的时候的cpu信息就会记录到XXX.prof中了。

2)查看监控数据

两种方式:
1 WEB上直接查看,浏览器直接访问:http://XXX:6060/debug/pprof
2 通过 go tool pprof 查看
具体的数据字段含义参考
https://blog.csdn.net/m0_38132420/article/details/71699815
https://studygolang.com/articles/9940

3) FreeOSMemory()

折腾和很久,发现两种方式监控到的内存都比较稳定。但是TOP命令查看RES还是一直在增长。
查了很久的资料:
https://groups.google.com/forum/#!topic/Golang-Nuts/kuS4kLCwkbE
https://stackoverflow.com/questions/37382600/cannot-free-memory-once-occupied-by-bytes-buffer
https://golang.org/pkg/runtime/debug/#FreeOSMemory

最后发现,golang的内存即使被gc回收了,也不会立刻归还给OS的,除非你手动调用FreeOSMemory()
这也是RES内存一起比预期中高的原因

Some things to clear. Go is a garbage collected language, which means that memory allocated and used by variables is automatically freed by the garbage collector when those variables become unreachable (if you have another pointer to the variable, that still counts as "reachable").
Freed memory does not mean it is returned to the OS. Freed memory means the memory can be reclaimed, reused for another variable if there is a need. So from the operating system you won't see memory decreasing right away just because some variable became unreachable and the garbage collector detected this and freed memory used by it.
The Go runtime will however return memory to the OS if it is not used for some time (which is usually around 5 minutes). If the memory usage increases during this period (and optionally shrinks again), the memory will most likely not be returned to the OS

FreeOSMemory forces a garbage collection followed by an attempt to return as much memory to the operating system as possible. (Even if this is not called, the runtime gradually returns memory to the operating system in a background task.)