前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#99 Not understanding how the GC works

Go语言中常见100问题-#99 Not understanding how the GC works

作者头像
数据小冰
发布2024-02-23 17:05:31
1330
发布2024-02-23 17:05:31
举报
文章被收录于专栏:数据小冰数据小冰
不了解GC工作原理

垃圾回收♻️(GC)是Go语言关键特性,帮助开发人员大大减轻心智负担。我们知道内存有栈和堆区之分,栈内存无需手动释放,但是堆内存需要我们手动释放。在Go语言中,GC会跟踪和释放不再使用的堆内存,每个Gopher都应该了解其工作原理,这非常有助于我们对程序进行优化。

概念

GC记录了一个对象引用关系树。Go语言中GC采用的是标记-清除算法,主要包含以下两个阶段:

  • 标记阶段:遍历堆中所有对象,并标记它们是否仍在使用
  • 清除解读:从根开始遍历引用关系树,并释放不再被引用的对象

当GC运行时,某些动作需要在stop the world情况下才能进行(准确的说,每次GC需要两次stop the world)。启动stop the world后,所有可用的CPU时间都用于执行GC,暂停应用代码执行,当这个阶段结束后会 start the world, 进入标记处理,并且恢复应用程序执行。在清扫阶段,清理处理和应用执行也是并发进行的。所以Go GC被称为并发标记和清除。

Go GC支持在消耗大量内存后释放内存。假设我们的应用程序有如下两个特征:

  • 在初始阶段,频繁分配内存占用了大量堆内存。
  • 在运行时阶段,适度分配内存占用少量堆内存。

那如何处理这种在开始阶段占用大量内存而后续占用较少的情况呢?Go GC采用定时清理机制。一段时间后,检测到不再需要这么大的堆内存,释放一些内存并将其返回给操作系统。

注意,如果内存清理不够快,可以使用 debug.FreeOSMemory()手动强制将内存返回给操作系统。

问题来了,定时清理周期是多少呢?与Java等其它语言相比,Go语言GC设置相当简单,只依赖GOGC环境变量。该变量默认值是100%,即触发下一次GC时,堆内存占用是上一次GC时两倍。

例如,假设刚刚触发GC时,占用的堆内存是128MB,如果GOGC=100,则当堆达到256MB时,触发下一次GC。默认情况下,每当堆大小加倍时会执行一次GC。

此外Go GC还有基于时间的清理策略,每2分钟会执行一次。

在生产环境中,可以根据负载情况,对GOGC进行微调:

  • 减少GOGC时,峰值内存会降低,但代价是产生更多CPU开销。
  • 增大GOGC时,峰值内存会增加,CPU开销减少。

我们可以设置GODEBUG环境变量来打印GC执行轨迹,例如在进行基准测试时,按如下设置启动GC跟踪。启动gctrace后,在每次GC运行时会向stderr写入GC信息。

代码语言:javascript
复制
GODEBUG=gctrace=1 go test -bench=. -v
实例

假设有一些公共服务向用户开放,在中午12点的高峰时段,有100万用户使用这些服务。从开始到达到高峰过程,接入用户是稳步增加的。下图反映的是当GOGC设置为100时,堆内存平均占用大小以及何时触发GC情况。

由于GOGC设置为100,所以每当堆占用大小加倍时,GC都会被触发。在这种情况下,再加上接入的用户数量是稳步增加的,整天的GC次数如下图所示,没有达到很高频率,在我们可接受范围内。

通过上面的GC频率图可以看到,在一天刚开始的时候GC次数从0增加到一个适度值,然后稳定保持一直到中午12点,后面用户数量开始减少,GC的频率也在稳步减少,这种情况下,设置GOGC为100没有问题。

现在,考虑另一种情况,假设100万用户差不多在一小时内全部接入,如下图所示,在上午8点时,堆内存平均大小迅速飙升,一小时后达到峰值。

在这一小时内,GC频率突然飙升,如下图所示。这是堆内存突然显著增加导致的。虽然Go GC是并发的,但是有stop the world,会导致大量的停顿,对我们的业务造成影响,例如会增加用户请求的平均延迟。

如何处理这种情况呢?可以考虑将GOGC设置为较大值来减轻GC压力。注意,增加GOGC带来的收益并不是线性的,因为GOGC设置的越大,累积的堆内存可能越大,清理的时间会越长。在生产环境,更改GOGC要慎重。

在一些更极端情况下,调整GOGC可能还不够。例如,用户量从0到100万不是在一个小时,而是几秒钟内完成,在这几秒内,GC的频率可能会达到极高状态,导致应用的性能非常差。

如果知道堆峰值,有一个技巧使用:强制分配大量的内存来提高稳定性。例如,我们可以在main.go中使用一个全局变量强制分配1GB内存。

代码语言:javascript
复制
var min = make([]byte, 1_000_000_000) 

这样写意义是啥呢?如果GOGC值是100, 只会在堆达到2GB时触发一次GC。这样会减少用户接入时触发GC的次数,减少对用户访问延迟影响。

有人会担心使用该技巧会浪费大量内存,事实并非如此。在大多数操作系统上,分配变量min并不会让应用消耗1GB内存,调用make底层调用的是mmap()系统调用,采用的是惰性分配。例如,在linux系统上,内存是通过页表寻址和映射转换的,使用mmap()在虚拟地址空间上分配1GB内存,而不是物理空间。在读取或写入产生page fault,从而真正分配物理内存。因此,即使应用在没有任何用户接入时启动,也不会消耗1GB物理内存,我们可以使用ps命令观察验证这种行为。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 不了解GC工作原理
    • 概念
      • 实例
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档