Go 都在什么时候触发GC,能手动触发GC吗?

Go 语言作为一门新语言,在早期经常遭到唾弃的就是在垃圾回收(下称:GC)机制中 STW(Stop-The-World)的时间过长。

那么这个时候,我们又会好奇一点,作为 STW 的起始,Go 语言中什么时候才会触发 GC 呢?

今天就由煎鱼带大家一起来学习研讨一轮。

什么是 GC

在计算机科学中,垃圾回收(GC)是一种自动管理内存的机制,垃圾回收器会去尝试回收程序不再使用的对象及其占用的内存。

最早 John McCarthy 在 1959 年左右发明了垃圾回收,以简化 Lisp 中的手动内存管理的机制(来自 @wikipedia)。

3cf61a0aa7d43b585e4ec8393cbaa0c6.png图来自网络

为什么要 GC

手动管理内存挺麻烦,管错或者管漏内存也很糟糕,将会直接导致程序不稳定(持续泄露)甚至直接崩溃。

GC 触发场景

GC 触发的场景主要分为两大类,分别是:

  1. 系统触发:运行时自行根据内置的条件,检查、发现到,则进行 GC 处理,维护整个应用程序的可用性。

  2. 手动触发:开发者在业务代码中自行调用 runtime.GC 方法来触发 GC 行为。

系统触发

在系统触发的场景中,Go 源码的 src/runtime/mgc.go 文件,明确标识了 GC 系统触发的三种场景,分别如下:

const (
 gcTriggerHeap gcTriggerKind = iota
 gcTriggerTime
 gcTriggerCycle
)
  • gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。

  • gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。-时间周期以 runtime.forcegcperiod 变量为准,默认 2 分钟。

  • gcTriggerCycle:如果没有开启 GC,则启动 GC。

  • 在手动触发的 runtime.GC 方法中涉及。

手动触发

在手动触发的场景下,Go 语言中仅有 runtime.GC 方法可以触发,也就没什么额外的分类的。

6bfb27d3283efac3b953d4439d9f631d.png
图片

但我们要思考的是,一般我们在什么业务场景中,要涉及到手动干涉 GC,强制触发他呢?

需要手动强制触发的场景极其少见,可能会是在某些业务方法执行完后,因其占用了过多的内存,需要人为释放。又或是 debug 程序所需。

基本流程

在了解到 Go 语言会触发 GC 的场景后,我们进一步看看触发 GC 的流程代码是怎么样的,我们可以借助手动触发的 runtime.GC 方法来作为突破口。

核心代码如下:

func GC() {
 n := atomic.Load(&work.cycles)
 gcWaitOnMark(n)

 gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
  
 gcWaitOnMark(n + 1)

 for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {
  sweep.nbgsweep++
  Gosched()
 }
  
 for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 {
  Gosched()
 }
  
 mp := acquirem()
 cycle := atomic.Load(&work.cycles)
 if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {
  mProf_PostSweep()
 }
 releasem(mp)
}
  1. 在开始新的一轮 GC 周期前,需要调用 gcWaitOnMark 方法上一轮 GC 的标记结束(含扫描终止、标记、或标记终止等)。

  2. 开始新的一轮 GC 周期,调用 gcStart 方法触发 GC 行为,开始扫描标记阶段。

  3. 需要调用 gcWaitOnMark 方法等待,直到当前 GC 周期的扫描、标记、标记终止完成。

  4. 需要调用 sweepone 方法,扫描未扫除的堆跨度,并持续扫除,保证清理完成。在等待扫除完毕前的阻塞时间,会调用 Gosched 让出。

  5. 在本轮 GC 已经基本完成后,会调用 mProf_PostSweep 方法。以此记录最后一次标记终止时的堆配置文件快照。

  6. 结束,释放 M。

在哪触发

看完 GC 的基本流程后,我们有了一个基本的了解。但可能又有小伙伴有疑惑了?

本文的标题是 “GC 什么时候会触发 GC”,虽然我们前面知道了触发的时机。但是....Go 是哪里实现的触发的机制,似乎在流程中完全没有看到?

监控线程

实质上在 Go 运行时(runtime)初始化时,会启动一个 goroutine,用于处理 GC 机制的相关事项。

代码如下:

func init() {
 go forcegchelper()
}

func forcegchelper() {
 forcegc.g = getg()
 lockInit(&forcegc.lock, lockRankForcegc)
 for {
  lock(&forcegc.lock)
  if forcegc.idle != 0 {
   throw("forcegc: phase error")
  }
  atomic.Store(&forcegc.idle, 1)
  goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)
    // this goroutine is explicitly resumed by sysmon
  if debug.gctrace > 0 {
   println("GC forced")
  }

  gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
 }
}

在这段程序中,需要特别关注的是在 forcegchelper 方法中,会调用 goparkunlock 方法让该 goroutine 陷入休眠等待状态,以减少不必要的资源开销。

在休眠后,会由 sysmon 这一个系统监控线程来进行监控、唤醒等行为:

func sysmon() {
 ...
 for {
  ...
  // check if we need to force a GC
  if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
   lock(&forcegc.lock)
   forcegc.idle = 0
   var list gList
   list.push(forcegc.g)
   injectglist(&list)
   unlock(&forcegc.lock)
  }
  if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now {
   lasttrace = now
   schedtrace(debug.scheddetail > 0)
  }
  unlock(&sched.sysmonlock)
 }
}

这段代码核心的行为就是不断地在 for 循环中,对 gcTriggerTimenow 变量进行比较,判断是否达到一定的时间(默认为 2 分钟)。

若达到意味着满足条件,会将 forcegc.g 放到全局队列中接受新的一轮调度,再进行对上面 forcegchelper 的唤醒。

堆内存申请

在了解定时触发的机制后,另外一个场景就是分配的堆空间的时候,那么我们要看的地方就非常明确了。

那就是运行时申请堆内存的 mallocgc 方法。核心代码如下:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
 shouldhelpgc := false
 ...
 if size <= maxSmallSize {
  if noscan && size < maxTinySize {
   ...
   // Allocate a new maxTinySize block.
   span = c.alloc[tinySpanClass]
   v := nextFreeFast(span)
   if v == 0 {
    v, span, shouldhelpgc = c.nextFree(tinySpanClass)
   }
   ...
   spc := makeSpanClass(sizeclass, noscan)
   span = c.alloc[spc]
   v := nextFreeFast(span)
   if v == 0 {
    v, span, shouldhelpgc = c.nextFree(spc)
   }
   ...
  }
 } else {
  shouldhelpgc = true
  span = c.allocLarge(size, needzero, noscan)
  ...
 }

 if shouldhelpgc {
  if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
   gcStart(t)
  }
 }

 return x
}
  • 小对象:如果申请小对象时,发现当前内存空间不存在空闲跨度时,将会需要调用 nextFree 方法获取新的可用的对象,可能会触发 GC 行为。

  • 大对象:如果申请大于 32k 以上的大对象时,可能会触发 GC 行为。

总结

在这篇文章中,我们介绍了 Go 语言触发 GC 的两大类场景,并分别基于大类中的细分场景进行了一一说明。

一般来讲,我们对其了解大概就可以了。若小伙伴们对其内部具体实现感兴趣,也可以以文章中的代码具体再打开看。

但需要注意,很有可能 Go 版本一升级,可能又变了,学思想要紧!

你对 Go 语言的 GC 有什么想法呢,欢迎在评论区留言交流 :)

关注煎鱼,吸取他的知识 👆

96a1e09f934dfab63014bc5a44958efd.png

煎鱼,在公司负责微服务架构等相关工作推进和研发,Go圈知名博主, 畅销书《Go 语言编程之旅》的作者,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,记得点赞!

<think>好的,用户想了解GC的执行过程和垃圾回收机制。我需要结合提供的引用内容,整理出一个结构清晰的回答,并生成相关问题。首先,我需要确定用户可能关心的核心点,比如GC的基本原理、执行步骤、不同算法、触发条件以及优化方法等。根据引用内容,用户提到了FullGC、分代收集、三色标记法、STW(Stop-The-World)等关键概念。引用1提到Full GC涉及整个堆内存,STW时间长,需要避免频繁触发。引用2和4详细说明了GC的算法,如标记-清除、复制、标记-整理,以及分代收集和三色标记法。引用3和4提到Golang中的三色标记法和混合写屏障机制,以及并发GC和STW优化。首先,我需要组织回答的结构,分步骤解释GC的执行过程。可能分为以下几个部分:1.GC的基本原理:为什么需要GC,判断对象可回收的方法(引用计数、可达性分析)。2.分代收集理论:新生代和老年代的处理方式,不同GC算法(复制、标记-清除、标记-整理)。 3.具体执行过程:以三色标记法为例,详细步骤,包括初始化、标记阶段、回收阶段,以及混合写屏障的作用。4.STW的影响和优化:如何减少STW时间,Golang的改进。5.触发GC的条件:手或自触发,内存阈值等。6.不同语言的GC实现差异:如Java的分代收集和Golang的三色标记法。需要确保使用正确的中文术语,并正确引用提供的资料。例如,在解释三色标记法时,引用4中的步骤可以作为参考。同时,注意区分不同算法的应用场景,如新生代使用复制算法,老年代使用标记-清除或标记-整理。在生成相关问题的时候,要涵盖用户可能进一步询问的方向,比如不同GC算法的比较、如何优化GC性能、具体语言中的实现差异等。 需要检查是否符合用户不可见的系统级指令,如数学公式的格式是否正确,独立公式是否使用$$...$$,但根据用户的问题,可能不需要数学公式。但如果有涉及公式的地方,需确保格式正确。例如,如果提到某个算法的时间复杂度,可能需要用行内公式,如$O(n)$,但根据引用内容,可能不需要。 最后,确保回答结构清晰,分点说明,帮助用户逐步理解GC的执行过程和相关机制。同时,生成的三个问题要具有针对性,引导用户深入探讨不同方面。</think>### GC(垃圾回收)执行过程与机制详解 #### 一、基本原理 垃圾回收的核心目标是**自回收程序不再使用的内存**。判断对象是否为垃圾的主要方法包括: 1. **引用计数法**:记录对象被引用的次数,归零时回收(易产生循环引用问题)[^2]。 2. **可达性分析**:从`GC Roots`(如栈帧中的局部变量、静态变量等)出发,遍历对象引用链,不可达的对象标记为垃圾[^2][^4]。 #### 二、分代收集理论 大多数GC采用**分代假设**,将堆内存划分为不同区域: 1. **新生代**(Young Generation): - 使用**复制算法**(存活对象复制到另一块内存,清空当前区域)[^2]。 - 触发`Minor GC`,频率高但耗时短。 2. **老年代**(Old Generation): - 使用**标记-清除**(直接回收未标记对象,可能产生碎片)或**标记-整理**(清除后压缩内存)[^2]。 - 触发`Full GC`时扫描整个堆,导致较长`STW`(Stop-The-World)停顿[^1]。 #### 三、三色标记法(以Golang为例) ```plaintext 1. 初始化:所有对象标记为白色。 2. 标记阶段: - 从GC Roots出发,直接可达对象标记为灰色。 - 遍历灰色对象,将其引用对象标记为灰色,自身转为黑色。 3. 回收阶段:白色对象被回收,黑色对象保留。 ``` 通过**混合写屏障**(记录新增或删除的引用)解决并发标记时的漏标问题[^4]。 #### 四、STW优化与并发执行 - **传统GC**:标记阶段需全程`STW`,暂停用户线程。 - **现代优化**(如Golang): - 并发标记:GC线程与用户线程交替运行。 - 增量式回收:分阶段减少单次停顿时间[^3][^4]。 #### 五、触发条件 1. **手触发**:如调用`runtime.GC()`(Golang)。 2. **自触发**: - 新生代内存不足 → `Minor GC`。 - 老年代内存阈值 → `Full GC`[^1][^4]。 #### 六、语言实现差异 | 特性 | Java | Golang | |-----------------|-------------------------------------|---------------------------------| | **分代模型** | 明确划分新生代/老年代 | 无分代,统一堆内存 | | **算法** | 分代收集 + 多种算法组合 | 三色标记法 + 混合写屏障 | | **STW时间** | Full GC停顿较长 | 1.3版本后优化至微秒级[^3][^4] | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值