27

Golang分代GC的策略

 5 years ago
source link: https://studygolang.com/articles/20159?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇文章 中讲解了Go分代GC的实现思路,还有一个问题没有讲解,Go中分代GC(Garbage Collection)的策略,如何穿插使用 Minor GCMajor GC

为何要穿插使用?

vQJnUjj.png!web

file

图片仅用作说明,有些地方不是很严谨,也与Go的GC方式不完全相同。

因为每轮GC都会有新的存活对象,存活下来的对象即被认为是老年代对象,这些对象在执行 Minor GC 期间是不会被清扫的,这也就会导致使用的堆内存会越来越大。经过几轮GC之后有些老年代对象可能已经不可达,可以被回收了,这时就要进行 Major GC 来进行完整的清扫,不论老年代对象和新生代对象,只要不再存活,就会被清扫回收掉,达到释放内存的目的。 上一篇文章 也有说到, Go将分代GC算法与标记清扫算法相结合,实现了一个不移动的分代GC ,究竟是如何结合使用的,就是本篇文章所要讲的。

如何进行穿插使用

下面的代码就是是判断本轮GC执行 Minor GCMajor GC 判断的函数,当函数返回 true 时,执行 Full GCMajor GC ),返回 false 时,执行 Gen GCMinor GC )。

func isGCCycleFull() bool {
    if !gcGen {
        return true
    }

    if gen.spaceFull || gen.forceFullGC {
        return true
    }

    // If full GC are taking a reasonable amount of time do not
    // bother with a generational GC.
    percentGCTime := int(memstats.gc_cpu_fraction * 100)
    if percentGCTime < gen.highCostThreshold {
        return true
    }

    // The next cycle will be full during warmup.
    // Perhaps this should trigger as long as we are initializing which we can
    // defined as a monotonic increase in heap size.
    if gen.cycleCount < gen.warmupCount {
        // number of warmup GC to do before considering a generational GC.
        return true
    }

    if gen.countBased {
        if gen.cycleCount%gen.cycleModulus == 0 {
            return gen.countBasedGen
        } else {
            return !gen.countBasedGen
        }
    }

    if work.fullRunCount >= work.fullSwitchCount {
        // Do a generational GC at least every switchCount GCs.
        return false
    }

    // Consider heuristics based on previous mark / cons ratios.
    if work.fullMarkConsEWMA*1.1 < work.genMarkConsEWMA {
        // This forces a generational GC as soon as warmup is over since work.genMarkConsEWMA is 0
        return true
    }

    // Do a generational GC in the next cycle.
    return false
}

下面分段解释一些这个函数,深入分析一下Go 的分代GC策略

  1. gcGen 是一个 const 常量,表示是否开启分代GC模式,当未开启时和原生GC无区别,每次都是执行 Full GC
if !gcGen {
    return true
}
  1. spaceFull 是标记本轮 Gen GC 中存活的对象空间 与 上一次 Full GC 中释放的空间的一半 的大小关系,在一轮 Full GC 后可能有连续几次的 Gen GC ,这里的 Full GC 是指最近的一次 Full GC 。当本轮 Gen GC 中存活的对象空间大于上一次 Full GC 中释放的空间的一半时, spaceFull 就会被置成 trueforceFullGC 在用户手动调用 runtime.GC 时会被置成 true
if gen.spaceFull || gen.forceFullGC {
    return true
}
  1. percentGCTime 是GC所占CPU时间的百分比,即 gctrace 中的百分数,下面数据中的6%, highCostThreshold 在不开启调试模式,程序正常运行时的初始值为10,不会改变,即GC所占CPU时间的百分比小于10%时都不会进行 Gen GC ,调试模式中 highCostThreshold=1

    gc 24 @1.583s 6%: 0.014+33+0.008 ms clock, 0.11+0/62/54+0.069 ms cpu, 158->158->316 MB, 316 MB goal, 8 P

percentGCTime := int(memstats.gc_cpu_fraction * 100)
if percentGCTime < gen.highCostThreshold {
    return true
}
  1. cycleCount 表示应用程序当前共进行了多少轮的GC(包含 Full GCGen GC ), warmupCount 目前的初始值为4,不会改变。这条规则表示的意思是,在前4轮GC不要进行 Gen GC ,因为前几轮对象较少,堆内存也较小,进行 Gen GC 可能不会有收益,可以理解为在进行初始化为 Gen GC 做准备。
if gen.cycleCount < gen.warmupCount {
    // number of warmup GC to do before considering a generational GC.
    return true
}
  1. countBase 的初始值目前是 falsecountBasedGen 的初值目前是 falsecycleModulus 的初始值目前为2,不会改变。那么眼尖的人已经看出来了,这个分支没有用啊, countBase 一直为 false ,那么这个分支就永远不会走。这个分支可以理解为用来强制触发 Gen GC 来进行测试和实验数据获取的。试想一下如果这样设置 countBased=true , cycleModulus=2 , countBasedGen=false ,那么如果可以走到这个分支, Full GCGen GC 的穿插策略即是,每2轮作为一个循环,奇数轮返回 true 执行 Full GC ,偶数轮返回 false 执行 Gen GC
if gen.countBased {
    if gen.cycleCount%gen.cycleModulus == 0 {
        return gen.countBasedGen
    } else {
        return !gen.countBasedGen
    }
}
  1. 一起说下剩下的几条规则。 fullRunCount 表示自从上一轮 Gen GC 后执行了多少轮 Full GC ,同样,在一轮 Gen GC 后也有可能连续几次的 Full GCfullSwitchCount 是一个在程序中会根据情况进行更新的值,初始值为0,每次更新都会+8。 介绍几个概念

    EWMA(指数加权移动平均值):可以简单的认为是平均值,有兴趣的可以自己查下资料深入理解下。

    MarkConsEWMA:Mark是指本轮GC中的标记成本(使用的CPU时间),Con是指本轮GC中释放的内存量,MarkConsEWMA是Mark/Con这个值的EWMA,对于 Full GCGen GC 分别维护一个 MarkConsEWMA 。个人认为可以理解为GC的性价比,标记成本越小越好,释放的内存越多越好,所以 MarkConsEWMA 这个值越小越好。

    如果在 Full GC 后的第一轮 Gen GC 中,单轮的 MarkCons 大于 Full GCMarkConsEWMA ,或者 Gen GC 的总 MarkConsEWMA 大于 Full GC 的总 MarkConsEWMA ,即 Gen GC 的收益没有 Full GC 大时, 那么 fullSwitchCount 将会+8,即更倾向于多做 Full GC

    另外如果 Gen GC 的总 MarkConsEWMA 大于 Full GC 的总 MarkConsEWMA 的1.1倍时,也会进行 Full GC ,如果所有规则都没有匹配,默认进行 Gen GC

if work.fullRunCount >= work.fullSwitchCount {
    // Do a generational GC at least every switchCount GCs.
    return false
}
if work.fullMarkConsEWMA*1.1 < work.genMarkConsEWMA {
    // This forces a generational GC as soon as warmup is over since work.genMarkConsEWMA is 0
    return true
}
return false

isGCCycleFull函数返回值的更新时间点

在GC的标记完成之后,扫描开始之前的这个时间点会确定下一轮GC时 Full GC 还是 Gen GC

A GC cycle starts after mark is complete and sweeping is about to begin. It is at this point that the decision of whether the upcoming cycle will be generational or a full GC is made.

End

如有错误还请指出,欢迎交流讨论。

转载请注明出处,谢谢。

最后祝大家劳动节快乐。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK