目录
  • 1、什么是 gc
  • 2、为什么要 gc
  • 3、gc 触发场景

      在早期经常遭到唾弃的就是在垃圾回收(下称:gc)机制中 stw(stop-the-world)的时间过长。那么这个时候,我们又会好奇一点,作为 stw 的起始,go 语言中什么时候才会触发 gc 呢?

      1、什么是 gc

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

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

      2、为什么要 gc

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

      3、gc 触发场景

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

      • 系统触发:运行时自行根据内置的条件,检查、发现到,则进行 gc 处理,维护整个应用程序的可用性。
      • 手动触发:开发者在业务代码中自行调用 runtime.gc 方法来触发 gc 行为。

      3.1系统触发

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

      const ( 
       gctriggerheap gctriggerkind = iota 
       gctriggertime 
       gctriggercycle 
      ) 
      
      
      
      • gctriggerheap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。
      • gctriggertime:当距离上一个 gc 周期的时间超过一定时间时,将会触发。-时间周期以 runtime.forcegcperiod 变量为准,默认 2 分钟。
      • gctriggercycle:如果没有开启 gc,则启动 gc。

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

      3.2手动触发

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

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

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

      3.3 基本流程

      在了解到 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) 
      } 
      
      

      在开始新的一轮 gc 周期前,需要调用 gcwaitonmark 方法上一轮 gc 的标记结束(含扫描终止、标记、或标记终止等)。

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

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

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

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

      结束,释放 m。

      3.4 在哪触发

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

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

      4、监控线程

      实质上在 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 循环中,对 gctriggertime now 变量进行比较,判断是否达到一定的时间(默认为 2 分钟)。

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

      5、堆内存申请

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

      那就是运行时申请堆内存的 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 什么时候会触发 gc问题的文章就介绍到这了,更多相关go 什么时候会触发 gc?内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!