当前位置: 首页 > 科技观察

Go什么时候触发GC?

时间:2023-03-12 10:04:27 科技观察

本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。Go语言作为一门新兴语言,早期经常因为垃圾回收(以下简称:GC)机制中的STW(Stop-The-World)时间过长而被唾弃。那么这个时候,我们又会好奇了。作为STW的开始,Go语言什么时候会触发GC呢?今天建宇就带大家一起学习探讨。什么是GC在计算机科学中,垃圾回收(GC)是一种自动管理内存的机制。垃圾收集器试图回收程序不再使用的对象及其占用的内存。垃圾收集最早由JohnMcCarthy在1959年左右发明,作为一种简化Lisp中手动内存管理的机制(来自@wikipedia)。为什么GC要手动管理内存,很麻烦。管理不善或者内存泄漏也是非常糟糕的,会直接导致程序不稳定(不断泄漏)甚至直接崩溃。GC触发场景GC触发场景主要分为两类,即:系统触发:当运行时检查发现内置条件时,会进行GC处理,以维护整个应用程序的可用性。手动触发:开发者在业务代码中调用runtime.GC方法触发GC行为。系统触发在系统触发的场景下,Go源码的src/runtime/mgc.go文件明确标识了GC系统触发的三种场景,如下:达到阈值(控制器计算出的触发堆的大小)就会触发。gcTriggerTime:距离上一次GC循环的时间超过一定时间时触发。-时间段以runtime.forcegcperiod变量为准,默认为2分钟。gcTriggerCycle:如果没有启用GC,则启动GC。参与手动触发的runtime.GC方法。手动触发在手动触发的场景下,只能触发Go语言中的runtime.GC方法,没有额外的分类。但是我们要思考的是,一般在哪些业务场景下,我们需要手动去干预GC,强制触发呢?需要手动强制触发的场景极其少见,可能是在某些业务方法执行完之后,因为占用内存太大,需要手动释放。或者它是调试程序所必需的。基本流程了解了Go语言会触发GC的场景后,我们来详细了解一下触发GC的流程代码。我们可以使用手动触发的runtime.GC方法作为突破口。核心代码如下:funcGC(){n:=atomic.Load(&work.cycles)gcWaitOnMark(n)gcStart(gcTrigger{kind:gcTriggerCycle,n:n+1})gcWaitOnMark(n+1)foratomic.Load(&work.cycles)==n+1&&sweepone()!=^uintptr(0){sweep.nbgsweep++Gosched()}foratomic.Load(&work.cycles)==n+1&&atomic.Load(&mheap_.sweepers)!=0{Gosched()}mp:=acquirem()cycle:=atomic.Load(&work.cycles)ifcycle==n+1||(gcphase==_GCmark&&cycle==n+2){mProf_PostSweep()}releasem(mp)}在开始新的GC循环之前,需要调用gcWaitOnMark方法来标记上一轮GC的结束(包括扫描终止、标记或标记终止等)。开始新一轮的GC循环,调用gcStart方法触发GC行为,开始扫描标记阶段。需要调用gcWaitOnMark方法等待当前GC周期的扫描、标记、标记终止完成。需要调用sweepone方法扫描未扫描的heapspan,并不断扫描,确保清理完成。在横扫完成前的阻塞时间内,Gosched会被叫放弃。本轮GC基本结束后,会调用mProf_PostSweep方法。这记录了最后一次标记终止时堆配置文件的快照。完成,释放M。从哪里触发看完了GC的基本流程,我们有了一个基本的了解。但是可能有些朋友会有疑惑?这篇文章的标题是《GC什么时候触发GC》,虽然我们更早知道触发的时机。但是......Go在哪里实现了触发机制?好像完全看不到进程?监控线程本质上是在Go运行时(runtime)初始化时启动一个goroutine来处理GC机制相关的事情。代码如下:funcinit(){goforcegchelper()}funcforcegchelper(){forcegc.g=getg()lockInit(&forcegc.lock,lockRankForcegc)for{lock(&forcegc.lock)ifforcegc.idle!=0{throw("forcegc:phaseerror")}atomic.Store(&forcegc.idle,1)goparkunlock(&forcegc.lock,waitReasonForceGCIdle,traceEvGoBlock,1)//这个goroutine被sysmonifdebug.gctrace显式恢复>0{println("GCforced")}gcStart(gcTrigger{kind:gcTriggerTime,now:nanotime()})}}在这个程序中,需要特别注意的是在forcechelper方法中,会调用goparkunlock方法让goroutine进入休眠等待状态,减少不必要的资源开销。休眠后会使用一个名为sysmon的系统监控线程来监控、唤醒等行为:funcsysmon(){...for{...//checkifweneedtoforceaGCift:=(gcTrigger{kind:gcTriggerTime,now:now});t.test()&&atomic.Load(&forcegc.idle)!=0{lock(&forcegc.lock)forcegc.idle=0varlistgListlist.push(forcegc.g)injectglist(&list)unlock(&forcegc.lock)}ifdebug.schedtrace>0&&lasttrace+int64(debug.schedtrace)*1000000<=now{lasttrace=nowschedtrace(debug.scheddetail>0)}unlock(&sched.sysmonlock)}}这段代码的核心行为是不断检查gcTriggerTime和now变量进行比较判断是否到达某个时间(默认为2分钟)。如果满足,则表示满足条件,将forcegc.g放入全局队列接受新一轮调度,然后唤醒上面的forcegchelper。了解了堆内存申请的定时触发机制后,还有一个场景就是分配的堆空间,那么我们要看的地方就很清楚了。也就是运行时申请堆内存的mallocgc方法。核心代码如下:funcmallocgc(sizeuintptr,typ*_type,needzerobool)unsafe.Pointer{shouldhelpgc:=false...ifsize<=maxSmallSize{ifnoscan&&size