RELATEED CONSULTING
相关咨询
选择下列产品马上在线沟通
服务时间:8:30-17:00
你可能遇到了下面的问题
关闭右侧工具栏

新闻中心

这里有您想知道的互联网营销解决方案
go语言什么情况触发gc go语言运行时

Golang实验性功能SetMaxHeap 固定值GC

简单来说, SetMaxHeap 提供了一种可以设置固定触发阈值的 GC (Garbage Collection垃圾回收)方式

创新互联建站专注于企业全网营销推广、网站重做改版、曲麻莱网站定制设计、自适应品牌网站建设、HTML5电子商务商城网站建设、集团公司官网建设、外贸营销网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为曲麻莱等各大城市提供网站开发制作服务。

官方源码链接

大量临时对象分配导致的 GC 触发频率过高, GC 后实际存活的对象较少,

或者机器内存较充足,希望使用剩余内存,降低 GC 频率的场景

GC 会 STW ( Stop The World ),对于时延敏感场景,在一个周期内连续触发两轮 GC ,那么 STW 和 GC 占用的 CPU 资源都会造成很大的影响, SetMaxHeap 并不一定是完美的,在某些场景下做了些权衡,官方也在进行相关的实验,当前方案仍没有合入主版本。

先看下如果没有 SetMaxHeap ,对于如上所述的场景的解决方案

这里简单说下 GC 的几个值的含义,可通过 GODEBUG=gctrace=1 获得如下数据

这里只关注 128-132-67 MB 135 MB goal ,

分别为 GC开始时内存使用量 - GC标记完成时内存使用量 - GC标记完成时的存活内存量 本轮GC标记完成时的 预期 内存使用量(上一轮 GC 完成时确定)

引用 GC peace设计文档 中的一张图来说明

对应关系如下:

简单说下 GC pacing (信用机制)

GC pacing 有两个目标,

那么当一轮 GC 完成时,如何只根据本轮 GC 存活量去实现这两个小目标呢?

这里实际是根据当前的一些数据或状态去 预估 “未来”,所有会存在些误差

首先确定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100

heap_marked 为本轮 GC 存活量, gcpercent 默认为 100 ,可以通过环境变量 GOGC=100 或者 debug.SetGCPercent(100) 来设置

那么默认情况下 goal = 2 * heap_marked

gc_trigger 是与 goal 相关的一个值( gc_trigger 大约为 goal 的 90% 左右),每轮 GC 标记完成时,会根据 |Ha-Hg| 和实际使用的 cpu 资源 动态调整 gc_trigger 与 goal 的差值

goal 与 gc_trigger 的差值即为,为 GC 期间分配的对象所预留的空间

GC pacing 还会预估下一轮 GC 发生时,需要扫描对象对象的总量,进而换算为下一轮 GC 所需的工作量,进而计算出 mark assist 的值

本轮 GC 触发( gc_trigger ),到本轮的 goal 期间,需要尽力完成 GC mark 标记操作,所以当 GC 期间,某个 goroutine 分配大量内存时,就会被拉去做 mark assist 工作,先进行 GC mark 标记赚取足够的信用值后,才能分配对应大小的对象

根据本轮 GC 存活的内存量( heap_marked )和下一轮 GC 触发的阈值( gc_trigger )计算 sweep assist 的值,本轮 GC 完成,到下一轮 GC 触发( gc_trigger )时,需要尽力完成 sweep 清扫操作

预估下一轮 GC 所需的工作量的方式如下:

继续分析文章开头的问题,如何充分利用剩余内存,降低 GC 频率和 GC 对 CPU 的资源消耗

如上图可以看出, GC 后,存活的对象为 2GB 左右,如果将 gcpercent 设置为 400 ,那么就可以将下一轮 GC 触发阈值提升到 10GB 左右

前面一轮看起来很好,提升了 GC 触发的阈值到 10GB ,但是如果某一轮 GC 后的存活对象到达 2.5GB 的时候,那么下一轮 GC 触发的阈值,将会超过内存阈值,造成 OOM ( Out of Memory ),进而导致程序崩溃。

可以通过 GOGC=off 或者 debug.SetGCPercent(-1) 来关闭 GC

可以通过进程外监控内存使用状态,使用信号触发的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式进行堆内存使用的监测

可以通过调用 runtime.GC() 或者 debug.FreeOSMemory() 来手动进行 GC 。

这里还需要说几个事情来解释这个方案所存在的问题

通过 GOGC=off 或者 debug.SetGCPercent(-1) 是如何关闭 GC 的?

gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)

通过 GC trace 可以看出,上面所说的 goal 变成了一个很诡异的值 17592186044415

实际上关闭 GC 后, Go 会将 goal 设置为一个极大值 ^uint64(0) ,那么对应的 GC 触发阈值也被调成了一个极大值,这种处理方式看起来也没什么问题,将阈值调大,预期永远不会再触发 GC

那么如果在关闭 GC 的情况下,手动调用 runtime.GC() 会导致什么呢?

由于 goal 和 gc_trigger 被设置成了极大值, mark assist 和 sweep assist 也会按照这个错误的值去计算,导致工作量预估错误,这一点可以从 trace 中进行证明

可以看到很诡异的 trace 图,这里不做深究,该方案与 GC pacing 信用机制不兼容

记住,不要在关闭 GC 的情况下手动触发 GC ,至少在当前 Go1.14 版本中仍存在这个问题

SetMaxHeap 的实现原理,简单来说是强行控制了 goal 的值

注: SetMaxHeap ,本质上是一个软限制,并不能解决 极端场景 下的 OOM ,可以配合内存监控和 debug.FreeOSMemory() 使用

SetMaxHeap 控制的是堆内存大小, Go 中除了堆内存还分配了如下内存,所以实际使用过程中,与实际硬件内存阈值之间需要留有一部分余量。

对于文章开始所述问题,使用 SetMaxHeap 后,预期的 GC 过程大概是这个样子

简单用法1

该方法简单粗暴,直接将 goal 设置为了固定值

注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal

简单用法2

当不关闭 GC 时, SetMaxHeap 的逻辑是, goal 仍按照 gcpercent 进行计算,当 goal 小于 SetMaxHeap 阈值时不进行处理;当 goal 大于 SetMaxHeap 阈值时,将 goal 限制为 SetMaxHeap 阈值

注:通过上文所讲,触发 GC 实际上是 gc_trigger ,所以当阈值设置为 12GB 时,会提前一点触发 GC ,这里为了描述方便,近似认为 gc_trigger=goal

切换到 go1.14 分支,作者选择了 git checkout go1.14.5

选择官方提供的 cherry-pick 方式(可能需要梯子,文件改动不多,我后面会列出具体改动)

git fetch "" refs/changes/67/227767/3 git cherry-pick FETCH_HEAD

需要重新编译Go源码

注意点:

下面源码中的官方注释说的比较清楚,在一些关键位置加入了中文注释

入参bytes为要设置的阈值

notify 简单理解为 GC 的策略 发生变化时会向 channel 发送通知,后续源码可以看出“策略”具体指哪些内容

返回值为本次设置之前的 MaxHeap 值

$GOROOT/src/runtime/debug/garbage.go

$GOROOT/src/runtime/mgc.go

注:作者尽量用通俗易懂的语言去解释 Go 的一些机制和 SetMaxHeap 功能,可能有些描述与实现细节不完全一致,如有错误还请指出

Go 语言三色标记扫描对象是 DFS 还是 BFS?

最近在看左神新书 《Go 语言设计与实现》的垃圾收集器时产生一个疑惑,花了点时间搞清楚了记录一下。

Go 语言垃圾回收的实现使用了标记清除算法,将对象的状态抽象成黑色(活跃对象)、灰色(活跃对象中间状态)、白色(潜在垃圾对象也是所有对象的默认状态)三种,注意没有具体的字段标记颜色。

整个标记过程就是把白色对象标黑的过程:

1.首先将 ROOT 根对象(包括全局变量、goroutine 栈上的对象等)放入到灰色集合

2.选一个灰色对象,标成黑色,将所有可达的子对象放入到灰色集合

3.重复2的步骤,直到灰色集合中为空

下图是书上的插图,看上去是一个典型的深度优先搜索的算法。

下图是刘丹冰写的《Golang 修养之路》的插图,看上去是一个典型的广度优先搜索的算法。

我疑惑的点在于这个标记过程是深度优先算法还是广度优先算法,因为很多文章博客对此都没有很清楚的说明,作为学习者这种细节其实也不影响对整个 GC 流程的理解,但是这种细节我非常喜欢扣:)

对着书和源码摸索着大致找到了一个结果是深度优先。下面看下大致的过程,源码基于1.15.2版本:

gcStart 是 Go 语言三种条件触发 GC 的共同入口

启动后台标记任务

为每个处理器创建用于执行后台标记任务的 Goroutine

上面休眠的 G 会在调度循环中检查并唤醒执行

执行标记

gcw 是每个 P 独有的所以不用担心并发的问题 和 GMP、mcache 一样设计,减少锁竞争

尝试在全局列表中获取一个不为空的 buf

这是官方实现的无锁队列:)涨见识了,for 循环加原子操作实现栈的 pop

到这里从灰色集合中获取待扫描的对象逻辑说完了。找到对象了接着就是 scanobject(b, gcw) 了,里面有两段逻辑要注意

根据索引位置找到对象进行标色

尝试存入 gcwork 的缓存中,或全局队列中

无锁队列,for 循环加原子操作实现栈的 push

到这里把灰色对象标黑就完成了,又放回灰色集合接着扫下一个指针。

Go 语言设计与实现 垃圾收集器

Golang三色标记+混合写屏障GC模式全分析

【golang】内存逃逸常见情况和避免方式

因为如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,go实现了gc垃圾回收机制,但gc会影响程序运行性能,所以要尽量减少程序的gc操作。

1、在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。

2、发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。

3、在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。

4、因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。

5、在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。

1、go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口。

2、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。

3、预先设定好slice长度,避免频繁超出容量,重新分配。


名称栏目:go语言什么情况触发gc go语言运行时
URL分享:http://lswzjz.com/article/ddjpcdo.html