在优化go程序时,内存优化是其中一项很重要的内容,减轻gc的压力,能够极大的优化我们的程序运行效率。

今天先来看一下两个与gc相关的环境变量:gctraceGOGC

gctrace

gctrace本身是GODEBUG这个环境变量中的一个选项,用来开启gc日志的。每次当完成一次gc扫描时,就会打印出本次gc的相关信息,我们可以用来监控程序的内存情况

1
2
3
4
5
6
7
$ GODEBUG=gctrace=1 GO_BIN # 开启gctrace
gc 1 @1.569s 0%: 0.28+2.2+4.6 ms clock, 1.1+0/0.96/4.6+18 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 2 @3.124s 0%: 0.026+1.4+0.058 ms clock, 0.10+1.3/0.033/1.5+0.23 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
gc 3 @5.326s 0%: 0.58+0.53+0.12 ms clock, 2.3+0.36/0.44/0.30+0.48 ms cpu, 4->4->1 MB, 5 MB goal, 4 P
...
scvg1: inuse: 4, idle: 58, sys: 63, released: 0, consumed: 63 (MB)
...

每一行对应一次gc,具体的输出格式如下:

1
gc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P [(forced)]
  • gc #:第几轮gc,从1开始递增

  • @#s:程序总的运行时间,单位s

  • #%:从程序开始到现在运行gc的时间占比
  • #+#+# ms clock:对应 第一次STW,终止SWEEP、开启写屏障 + 并发MarkScan + 第二次STW,结束Mark 这三个阶段wall-clock的耗时,单位为ms
  • #+#/#/#+# ms cpu:对应 第一次STW + 并发标记:Assist Time/ 并发标记:Background GC time / 并发标记:Idle GC time + 第二次STW结束Mark 这几个阶段的cpu时间,单位ms
  • #->#-># MB:分别对应gc开始时的堆大小、gc结束时的堆大小以及live heapgc mark阶段标记为黑色的内存总量)的大小

  • # MB goal:目标heap size

  • # P:使用的P数量
  • (forced):如果调用runtime.GC()强制触发gc

除了输出gc的信息,当runtime向操作系统归还内存时,也会打印出信息,比如上面的:

1
scvg1: inuse: 4, idle: 58, sys: 63, released: 0, consumed: 63 (MB)
  • scvg#:第几次归还,从1开始计数
  • inuse: #:正在使用或部分被使用的spans的内存大小,单位MB
  • idle: #:空闲等待归还给操作系统的spans的内存大小,单位MB
  • sys: #:从操作系统映射的内存大小,实际上是gc堆可访问的内存虚拟地址空间,单位MB
  • released: #:本次归还给操作系统的内存大小,单位MB
  • consumed: #:从操作系统分配的内存大小,等于sys - released

GOGC

根据runtime/mgc.go中的注释:

Next GC is after we’ve allocated an extra amount of memory proportional to
the amount already in use. The proportion is controlled by GOGC environment variable
(100 by default). If GOGC=100 and we’re using 4M, we’ll GC again when we get to 8M
(this mark is tracked in next_gc variable). This keeps the GC cost in linear
proportion to the allocation cost. Adjusting GOGC just changes the linear constant
(and also the amount of extra memory used).

go1.5之前,运行gc mark阶段会stop the world, 能够根据next_gc变量(也就是goal heap size,可以直接通过GOGC变量调整)精确地控制堆内存的增长:

但是go1.5之后,gc mark可以跟用户协程并发运行,因此在gc执行过程中仍然会有新的内存被分配,因此gc的触发点需要相对next_gc提前:

如上图所示,Hm(n-1)表示上一次gc结束后的堆大小,而Hgnext_gc,而我们在Ht触发gc,因为gc过程中可能会有新的内存分配,当gc结束时,当前的堆大小为Hagogc实现,需要提供一种动态调整的机制,根据内存分配情况调整Ht的值,使得Ha能够与Hg尽量接近。

总体来说,我们可以通过设置GOGC的值来调整gc的触发阈值:

  • 当小于零或者等于off时,将会关闭gc
  • 设置较大的值:减少gc触发,但是会增加内存占用
  • 设置偏小的值:频繁触发gc