Golang GC算法

一剑光寒十九洲 · 大约12小时之前 · 46 次点击 · 预计阅读时间 13 分钟 · 不到1分钟之前 开始浏览

概括

Go的垃圾回收官方形容为 非分代 非紧缩 写屏障 三色并发标记清理算法。
非分代:不像Java那样分为年轻代和年老代,自然也没有minor gc和maj o gc的区别。
非紧缩:在垃圾回收之后不会进行内存整理以清除内存碎片。
写屏障:在并发标记的过程中,如果应用程序(mutator)修改了对象图,就可能出现标记遗漏的可能,写屏障就是为了处理标记遗漏的问题。
三色:将GC中的对象按照搜索的情况分成三种:

  1. 黑色: 对象在这次GC中已标记,且这个对象包含的子对象也已标记
  2. 灰色: 对象在这次GC中已标记, 但这个对象包含的子对象未标记
  3. 白色: 对象在这次GC中未标记
    并发:可以和应用程序(mutator)在一定程度上并发执行。
    标记清理:GC算法分为两个大步骤:标记阶段找出要回收的对象,清理阶段则回收未被标记的对象(要被回收的对象)

触发时机

  • gcTriggerAlways: 强制触发GC,没找到什么情况下使用这个
  • gcTriggerHeap: 当前分配的内存达到一定值(动态计算)就触发GC
  • gcTriggerTime: 当一定时间(2分钟)没有执行过GC就触发GC
  • gcTriggerCycle: 要求启动新一轮的GC, 已启动则跳过, 手动触发GC的runtime.GC()会使用这个条件
func gcStart(mode gcMode, trigger gcTrigger) {// Since this is called from malloc and malloc is called in// the guts of a number of libraries that might be holding// locks, don't attempt to start GC in non-preemptible or// potentially unstable situations.mp := acquirem()if gp := getg(); gp == mp.g0 || mp.locks > 1 || mp.preemptoff != "" {releasem(mp)return}releasem(mp)mp = nil// 检查GC条件是否满足,和下面的test()构成双检查锁,如果满足GC条件但目前处于GC清理阶段,那就参与清理for trigger.test() && gosweepone() != ^uintptr(0) {sweep.nbgsweep++}// 加锁检查semacquire(&work.startSema)if !trigger.test() {semrelease(&work.startSema)return}/***************  .....  *****************/}

在trigger.test()函数中,检查是否满足GC触发的条件

func (t gcTrigger) test() bool {if !memstats.enablegc || panicking != 0 {return false}if t.kind == gcTriggerAlways {return true}if gcphase != _GCoff {return false}switch t.kind {case gcTriggerHeap:// Non-atomic access to heap_live for performance. If// we are going to trigger on this, this thread just// atomically wrote heap_live anyway and we'll see our// own write.return memstats.heap_live >= memstats.gc_triggercase gcTriggerTime:if gcpercent < 0 {return false}lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))// forcegcperiod = 2分钟return lastgc != 0 && t.now-lastgc > forcegcperiodcase gcTriggerCycle:// t.n > work.cycles, but accounting for wraparound.return int32(t.n-work.cycles) > 0}return true
}
const (// gcTriggerAlways indicates that a cycle should be started// unconditionally, even if GOGC is off or we're in a cycle// right now. This cannot be consolidated with other cycles.gcTriggerAlways gcTriggerKind = iota// gcTriggerHeap indicates that a cycle should be started when// the heap size reaches the trigger heap size computed by the// controller.gcTriggerHeap// gcTriggerTime indicates that a cycle should be started when// it's been more than forcegcperiod nanoseconds since the// previous GC cycle.gcTriggerTime// gcTriggerCycle indicates that a cycle should be started if// we have not yet started cycle number gcTrigger.n (relative// to work.cycles).gcTriggerCycle
)

算法过程

  1. Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC
  2. Mark: 扫描所有根对象, 和根对象可以到达的所有对象, 标记它们不被回收
  3. Mark Termination: 完成标记工作, 重新扫描部分根对象(要求STW)
  4. Sweep: 按标记结果清扫span

    golang_gc.jpg

func gcStart(mode gcMode, trigger gcTrigger) {// 拿到锁,保证只有一个执行流进入到这个临界区semacquire(&worldsema)// 启动后台扫描任务(G)if mode == gcBackgroundMode {gcBgMarkStartWorkers()}gcResetMarkState()work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocsif work.stwprocs > ncpu {work.stwprocs = ncpu}work.heap0 = atomic.Load64(&memstats.heap_live)work.pauseNS = 0work.mode = modenow := nanotime()work.tSweepTerm = nowwork.pauseStart = nowif trace.enabled {traceGCSTWStart(1)}systemstack(stopTheWorldWithSema)// Finish sweep before we start concurrent scan.systemstack(func() {finishsweep_m()})// clearpools before we start the GC. If we wait they memory will not be// reclaimed until the next GC cycle.clearpools()work.cycles++if mode == gcBackgroundMode { // Do as much work concurrently as possiblegcController.startCycle()work.heapGoal = memstats.next_gc// Enter concurrent mark phase and enable// write barriers.setGCPhase(_GCmark)gcBgMarkPrepare() // Must happen before assist enable.gcMarkRootPrepare()// Mark all active tinyalloc blocks. Since we're// allocating from these, they need to be black like// other allocations. The alternative is to blacken// the tiny block on every allocation from it, which// would slow down the tiny allocator.gcMarkTinyAllocs()// At this point all Ps have enabled the write// barrier, thus maintaining the no white to// black invariant. Enable mutator assists to// put back-pressure on fast allocating// mutators.atomic.Store(&gcBlackenEnabled, 1)// Assists and workers can start the moment we start// the world.gcController.markStartTime = now// Concurrent mark.systemstack(func() {now = startTheWorldWithSema(trace.enabled)})work.pauseNS += now - work.pauseStartwork.tMark = now}semrelease(&work.startSema)
}

关键函数及路径:

  1. gcBgMarkStartWorkers():准备后台标记工作goroutine(allp), 启动后等待该任务通知信号量bgMarkReady再继续,notewakeup(&work.bgMarkReady)
  2. gcResetMarkState():重置一些全局状态和所有gorontine的栈(一种根对象)扫描状态
  3. systemstack(stopTheWorldWithSema):启动stop the world
  4. systemstack(func(){finishsweep_m()}): 不断去除要清理的span进行清理,然后重置gcmark位
  5. clearpools(): 清扫sched.sudogcache和sched.deferpool,不知道在干嘛......
  6. gcController.startCycle():启动新一轮GC,设置gc controller的状态位和计算一些估计值
  7. setGCPhase(_GCmark):设置GC阶段,启用写屏障
  8. gcBgMarkPrepare():设置后台标记任务计数;work.nproc = ^uint32(0),work.nwait = ^uint32(0)
  9. gcMarkRootPrepare(): 计算扫描根对象的任务数量
  10. gcMarkTinyAllocs(): 标记所有tiny alloc等待合并的对象
  11. atomic.Store(&gcBlackenEnabled, 1): 启用辅助GC
  12. systemstack(func(){now=startTheWorldWithSema(trace.enable)}): 停止stop the world
func gcBgMarkWorker(_p_ *p) {/**********  .......  ***********/// 通知gcBgMarkStartWorkers可以继续处理notewakeup(&work.bgMarkReady)for {// 切换到g0运行systemstack(func() {// Mark our goroutine preemptible so its stack// can be scanned. This lets two mark workers// scan each other (otherwise, they would// deadlock). We must not modify anything on// the G stack. However, stack shrinking is// disabled for mark workers, so it is safe to// read from the G stack.casgstatus(gp, _Grunning, _Gwaiting)switch _p_.gcMarkWorkerMode {default:throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")case gcMarkWorkerDedicatedMode:gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)if gp.preempt {lock(&sched.lock)for {gp, _ := runqget(_p_)if gp == nil {break}globrunqput(gp)}unlock(&sched.lock)}// Go back to draining, this time// without preemption.gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)case gcMarkWorkerFractionalMode:gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)case gcMarkWorkerIdleMode:gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)}casgstatus(gp, _Gwaiting, _Grunning)})/********   ......  ***********/// 判断是否所有后台标记任务都完成, 并且没有更多的任务if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {gcMarkDone()}}
}
  1. gcDrain()是执行标记的函数
  2. 当所有标记任务完成时,执行gcMarkDone()函数
func gcDrain(gcw *gcWork, flags gcDrainFlags) {initScanWork := gcw.scanWork// 如果根对象未扫描完,则先扫描根对象,Jobs为根对象总数,next相当于一个对象任务的取数器if work.markrootNext < work.markrootJobs {for !(preemptible && gp.preempt) {job := atomic.Xadd(&work.markrootNext, +1) - 1if job >= work.markrootJobs {break}// 将会扫描根对象,并把它加入到标记队列gcWork中之中,也就是把对象变成灰色markroot(gcw, job)if check != nil && check() {goto done}}}// 当根对象全部put到标记队列中, 消费标记队列,根据对象图进行消费for !(preemptible && gp.preempt) {if work.full == 0 {gcw.balance()}var b uintptrif blocking {b = gcw.get()} else {b = gcw.tryGetFast()if b == 0 {b = gcw.tryGet()}}if b == 0 {// work barrier reached or tryGet failed.break}scanobject(b, gcw)// 如果已经扫描了一定数量的对象(gcCreditSlack的值是2000)if gcw.scanWork >= gcCreditSlack {// 把扫描的对象数量添加到全局atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)// 减少辅助GC的工作量和唤醒等待中的Gif flushBgCredit {gcFlushBgCredit(gcw.scanWork - initScanWork)initScanWork = 0}idleCheck -= gcw.scanWorkgcw.scanWork = 0// 如果是idle模式且达到了检查的扫描量, 则检查是否有其他任务(G), 如果有则跳出循环if idle && idleCheck <= 0 {idleCheck += idleCheckThresholdif pollWork() {break}}}}done:// 把扫描的对象数量添加到全局if gcw.scanWork > 0 {atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)// 减少辅助GC的工作量和唤醒等待中的Gif flushBgCredit {gcFlushBgCredit(gcw.scanWork - initScanWork)}gcw.scanWork = 0}
}
func gcMarkDone() {semacquire(&work.markDoneSema)// Re-check transition condition under transition lock.if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {semrelease(&work.markDoneSema)return}// 暂时禁止启动新的后台标记任务atomic.Xaddint64(&gcController.dedicatedMarkWorkersNeeded, -0xffffffff)prevFractionalGoal := gcController.fractionalUtilizationGoalgcController.fractionalUtilizationGoal = 0// 转换到Mark Termination阶段,进入STW阶段systemstack(stopTheWorldWithSema)// 标记对根对象的扫描已完成work.markrootDone = true// 禁止辅助GC和后台任务atomic.Store(&gcBlackenEnabled, 0)// 唤醒所有因为辅助GC而休眠的GgcWakeAllAssists()semrelease(&work.markDoneSema)// 计算下一次触发gc需要的heap大小nextTriggerRatio := gcController.endCycle()// 计算下一次触发gc需要的heap大小gcMarkTermination(nextTriggerRatio)
}
func gcMarkTermination(nextTriggerRatio float64) {// 禁止辅助GC和后台标记任务的运行// 重新允许本地标记队列(下次GC使用)// 设置当前GC阶段到完成标记阶段, 并启用写屏障atomic.Store(&gcBlackenEnabled, 0)gcBlackenPromptly = falsesetGCPhase(_GCmarktermination)systemstack(func() {gcMark(startTime)})systemstack(func() {// 设置当前GC阶段到关闭, 并禁用写屏障setGCPhase(_GCoff)// 唤醒后台清扫任务, 将在STW结束后开始运行gcSweep(work.mode)})// 更新下一次触发gc需要的heap大小(gc_trigger)gcSetTriggerRatio(nextTriggerRatio)// 重置清扫状态sweep.nbgsweep = 0sweep.npausesweep = 0// 统计执行GC的次数然后唤醒等待清扫的Glock(&work.sweepWaiters.lock)memstats.numgc++injectglist(work.sweepWaiters.head.ptr())work.sweepWaiters.head = 0unlock(&work.sweepWaiters.lock)// 重新启动世界systemstack(func() { startTheWorldWithSema(true) })// 移动标记队列使用的缓冲区到自由列表, 使得它们可以被回收prepareFreeWorkbufs()// 释放未使用的栈systemstack(freeStackSpans)semrelease(&worldsema)// 重新允许当前的G被抢占releasem(mp)mp = nil

当标记的扫描工作完成之后,会进入到GC Mark Termination阶段,也就是gcMarkDone()函数,关键路径:

  1. systemstack(stopTheWorldWithSema):启动STW
  2. gcWakeAllAssists():唤醒所有因辅助gc而休眠的G
  3. nextTriggerRatio:=gcController.endCycle():计算下一次触发gc需要的heap大小
  4. setGCPhase(_GCmarktermination):启用写屏障
  5. systemstack(func() {gcMark(startTime)}): 再次执行标记
  6. systemstack(func(){setGCPhase(_GCoff);gcSweep(work.mode)}):关闭写屏障,唤醒后台清扫任务,将在STW结束后开始运行
  7. gcSetTriggerRatio(nextTriggerRatio):更新下次触发gc时的heap大小
  8. systemstack(func() { startTheWorldWithSema(true) }): 停止STW

STW分析:web程序中,我们关注最大停顿时间

STW出现在两个位置,分别是在初始标记阶段Mark和并发标记完成后重标记Mark Termination:

初始标记阶段:

  • systemstack(stopTheWorldWithSema):启动stop the world
  • systemstack(func(){finishsweep_m()}): 不断去除要清理的span进行清理,然后重置gcmark位
  • clearpools(): 清扫sched.sudogcache和sched.deferpool,不知道在干嘛......
  • gcController.startCycle():启动新一轮GC,设置gc controller的状态位和计算一些估计值
  • gcMarkRootPrepare(): 计算扫描根对象的任务数量
  • gcMarkTinyAllocs(): 涂灰所有tiny alloc等待合并的对象
  • systemstack(func(){now=startTheWorldWithSema(trace.enable)}): 停止stop the world

找出其中比较耗时的阶段:

  • finishsweep_m():如果上一次GC清扫阶段没有完成,那么在新的一轮GC阶段中就会在阻塞在这里,使得原本可以和应用程序并行的清扫阶段被放进STW。所以,如果频繁的执行GC,就可能会使得GC的最大停顿时间变长。
  • clearpools():时间复杂度大概为:O(5*L),L为_defer中链表的长度。
  • gcController.startCycle():O(P),P为go的P的数量,和cpu数有关,时间复杂度可以忽略
  • gcMarkRootPrepare(): O(全局变量区),包括bss段和data段
  • gcMarkTinyAllocs(): O(P)

个人觉得,对STW影响最大的是finishsweep_m()阶段,所有我们应该尽量避免让go在清扫期执行新一轮的GC。

重新标记阶段

  • systemstack(stopTheWorldWithSema):启动STW
  • gcWakeAllAssists():唤醒所有因辅助gc而休眠的G
  • nextTriggerRatio:=gcController.endCycle():计算下一次触发gc需要的heap大小
  • setGCPhase(_GCmarktermination):启用写屏障
  • systemstack(func() {gcMark(startTime)}): 再次执行标记
  • systemstack(func(){setGCPhase(_GCoff);gcSweep(work.mode)}):关闭写屏障,唤醒后台清扫任务,将在STW结束后开始运行
  • gcSetTriggerRatio(nextTriggerRatio):更新下次触发gc时的heap大小
  • systemstack(func() { startTheWorldWithSema(true) }): 停止STW

找出其中比较耗时的阶段:

  • gcWakeAllAssists():O(G),将所有可运行的G插入到调度链表
  • systemstack(func() {gcMark(startTime)}):

Golang GC算法相关推荐

  1. 图解Golang的GC算法

    图解Golang的GC算法 原创 RyuGou 程序猿菜刚RyuGou 2019-03-10 来源:https://mp.weixin.qq.com/s/_h0-8hma5y_FHKBeFuOOyw ...

  2. golang GC垃圾回收机制

    ** golang GC垃圾回收 ** 垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行. Go ...

  3. Golang GC概述

    垃圾回收 垃圾回收器一直是被诟病最多,也是整个运行中改进最努力的部分.所有变化都是为了缩短STW时间,提高程序实时性. 大事记: 2014年6月 1.3并发清理 2015年8月 1.5三色并发标记 上 ...

  4. 深入理解 Java 虚拟机 - 你了解 GC 算法原理吗

    来自:好好学Java 虚拟机系列文章 深入理解 Java 虚拟机(第一弹) - Java 内存区域透彻分析 深入理解 Java 虚拟机(第二弹) - 常用 vm 参数分析 深入理解 Java 虚拟机- ...

  5. Jvm 系列(三):GC 算法 垃圾收集器

    这篇文件将给大家介绍GC都有哪几种算法,以及JVM都有那些垃圾回收器,它们的工作原理. 概述 垃圾收集 Garbage Collection 通常被称为"GC",它诞生于1960年 ...

  6. java标志清理_JVM内存管理之GC算法精解(五分钟让你彻底明白标记/清除算法)...

    相信不少猿友看到标题就认为LZ是标题党了,不过既然您已经被LZ忽悠进来了,那就好好的享受一顿算法大餐吧.不过LZ丑话说前面哦,这篇文章应该能让各位彻底理解标记/清除算法,不过倘若各位猿友不能在五分钟内 ...

  7. 《深入理解java虚拟机》笔记2——GC算法与内存分配策略

    说起垃圾收集(Garbage Collection, GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Sca ...

  8. 深入理解JVM(2)——GC算法与内存分配策略

    说起垃圾收集(Garbage Collection, GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Sca ...

  9. JVM(3):Java GC算法 垃圾收集器

    概述 垃圾收集 Garbage Collection 通常被称为"GC",它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计数 ...

最新文章

  1. 算法---------两数之和
  2. 输入参数的数目不足_机器学习算法—KMEANS算法原理及阿里云PAI平台算法模块参数说明...
  3. 【问链-EOS公开课】第十二课 EOS整体代码结构
  4. 手机在线测试黄疸软件,在家怎么用手机测黄疸
  5. 把数据库中的数据制作成Excel数据
  6. 学习 Message(11): 测试 TWMMouse 结构相关的鼠标消息
  7. Matlab Tricks(六)—— 矩阵乘法的实现
  8. Android深入四大组件(一)应用程序启动过程
  9. 正点原子STM32(基于标准库)
  10. 股票指标(摘自同花顺软件)
  11. 计算机点击桌面无反应,点击显示桌面没反应? 显示桌面没反应解决方法
  12. YNWA,同样是我们普通人的鞭策
  13. Thinkphp6 Malformed UTF-8 characters, possibly incorrectly encoded in
  14. python图像算法工程师_图像算法工程师的岗位职责
  15. 【Typescript专题】之类型进阶
  16. 对单位下三角矩阵的意外发现
  17. 武汉大学信管专业期末复习系列——《计算机网络》(谢希仁版)(网络层)
  18. 和刘备相关的人(四)
  19. 作为开发用的GUI音频处理软件推荐--wavosaur
  20. 比尔盖茨给青年人的十个忠告

热门文章

  1. 通过程序压缩/解压文件
  2. [英语阅读]悉尼受70年一遇红色沙暴袭击
  3. 基于Linux的服务器搭建
  4. HTML5大纲算法(HTML5 Outliner)
  5. .misc 可爱的故事
  6. 微型计算机多少瓦,微型计算机能效限定值及能效等级 GB28380-2012
  7. 1秒把 FLV MOV AVI MKV 3GP WEBM 转去 MP4 完全免费 - 完美教程 超级简单 你没看错
  8. 微星笔记本怎么重装系统教程
  9. JavaScript算法题100道
  10. android url scheme 跳转传值,如何自定义 URL Scheme 进行跳转