2022-03-14 Go垃圾回收之旅3 - 静态编译
Go的源码会被编译成二进制文件,然后直接在对应的操作系统上运行。那么,这对学习GC有什么意义呢?让我们一起看看今天的内容。
《Go垃圾回收之旅》原文链接 - https://go.dev/blog/ismmkeynote
我们先和JAVA程序做个对比:
- Go
- Go编译的二进制文件
- Linux
- JAVA
- Java打包的JAR文件
- JVM
- Linux
从这个架构不难猜到,上文谈到的 运行时,Go语言是直接编译到二进制文件里的;而JAVA是在JVM里实现的。
Go的这种实现方式,主要优劣点如下:
- 优点: 程序的运行更具备 确定性,即开发人员可以根据代码,预测到程序的运行逻辑,更容易针对性地优化
- 缺点:运行时没有
JIT
机制,无法针对具体的运行结果进行反馈优化
JIT
的优化方向很多,我这里举一个热点函数优化的例子:
- 在代码中,函数f需要输入参数a和b
- 运行了一段时间后,
JIT
发现b的输入参数一直都是某个固定值b1 - 这时,
JIT
进行编译优化,将函数f编译成一个新函数f1- f1只需要入参a
- b参数被替换为固定值b1
- 减少参数复杂度,能提升程序效率,尤其是热点函数
- 如果参数b突然变成了b2,那
JIT
就会从f1回退到f
简单来说:Go程序会怎么运行,往往在编码阶段就可以预期到了;而JAVA引入的JIT
能力,可以在程序运行后,根据具体的运行情况,做针对性地优化,提升效率的同时也带了很多的不确定性。
两种实现方式各有利弊,团队可以根据实际情况自行选择。单从Go语言开发者来说,排查线上问题相对有JIT
机制的JAVA程序简单很多。
这种确定性也让Go的GC相对简单不少,方便我们的学习。
2022-03-15 Go垃圾回收之旅4 - 性能压力下的Go程序
在这篇演讲中,有这么一段很有意思的描述:
Out of memory, OOMs, are tough on Go;
temporary spikes in memory usage should be handled by increasing CPU costs, not by aborting.
Basically if the GC sees memory pressure it informs the application that it should shed load. Once things are back to normal the GC informs the application that it can go back to its regular load.
这段话包含了Go语言的GC,在面对CPU和内存压力下的决策:
- Go程序很少会OOM
- 这句话有一定前提,即内存设置是合理的,代码也没有明显的内存泄露问题
- 至于具体原因,我们看下文
- 业务高峰时内存使用率过高,应该通过提升CPU能力来解决,而不是中止程序
- 自动GC是需要CPU的计算资源做支持,来清理无用内存
- 要保证内存资源能支持程序的正常运行,有两个思路:
- 减少已有内存 - 通过GC来回收无用的内存
- 限制新增内存 - 即运行时尽可能地避免新内存的分配,最简单的方法就是不运行代码
- 显然,中止程序对业务的影响很大,我们更倾向于通过GC去回收内存,腾出新的空间
- GC压力高时,通知应用减少负载;而当恢复正常后,GC再通知应用可以恢复到正常模式了
- 我们可以将上述分为两类工作
- 业务逻辑的Goroutine
- GC的Goroutine
- 这两类Goroutine都会消耗CPU资源,区别在于:
- 运行业务逻辑往往会增加内存
- GC是回收内存
- 这里就能体现出Go运行时的策略
- 内存压力高时,GC线程更容易抢占到CPU资源,进行内存回收
- 代价是业务处理逻辑会有一定性能损耗,被分配的计算资源减少
- 我们可以将上述分为两类工作
GC最直观的影响就体现在延迟上。尤其是在STW - Stop The World情况下,程序会暂停所有非GC的工作,进行全量的垃圾回收。即便整个GC只花费了1s,所有涉及到这个程序的业务调用,都会增加1s延迟;在微服务场景下,这个问题会变得尤为复杂。
而GC的方案迭代,最直观的效果就体现在这个延迟优化上。
2022-03-17 Go垃圾回收之旅5 - GC Pacer
今天我们会重点讨论Go语言GC Pacer这个概念。
《Go垃圾回收之旅》原文链接 - https://go.dev/blog/ismmkeynote
要理解透彻GC Pacer的非常困难,底层实现细节必须深入到源码。这里,我们会通过分享中的关键性描述,来思考GC Pacer的设计理念。
It is basically based on a feedback loop that determines when to best start a GC cycle.
我们聚焦到两个词:
feedback loop
反馈循环,GC Pacer是会根据实际GC情况会不断迭代、反馈的when to best start a GC cycle
强调了GC Pacer的目标 - 为了决定一个最佳启动GC的时机
GC Pacer的内部原理也和它的定义非常贴切,它是根据步长来决定GC的:
- 对象:堆上的内存分配
- 步长:设定值,如100%
- 触发时机:当前堆上内存大小 >= 上次堆上内存大小 * (1 + 100%)
简单来说,就是一种 按比例增长 的触发机制。但这个机制没有那么简单,我们看下面这段:
If need be, the Pacer slows down allocation while speeding up marking.
At a high level the Pacer stops the Goroutine, which is doing a lot of the allocation, and puts it to work doing marking.
这两句描述和我们上一讲的内容对应上了 - 在一定的性能压力下,Pacer会减少内存的分配,而花更多的时间在对象的标记(marking)上,它是GC里的最耗性能的步骤。
对应到上面提到的反馈呢,也就是GC Pacer并不是单纯的一种 按比例增长 的触发机制,还有一些其余因素的影响:比如,当前这次的GC花费的CPU计算资源与标记的耗时超过了预期,表示当前整个GC存在一定压力,下次的GC的开始时间需要适当提前。
GC Pacer最近也重新做了一次大的改动,有兴趣的可以参考这篇文章:
深入研究GC Pacer需要很多数学知识储备,留给有兴趣的同学自行探索了。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding