原文链接:Avoiding high GC overhead with large heaps
Go的Garbage Collector(GC)在分配的内存量相对较小时工作得非常好,但是如果堆较大,GC最终可能会占用大量的CPU,在极端情况下,它甚至可能无法跟上节奏。
What‘s the problem?
GC的工作就是确定哪些内存可以释放,它是通过扫描内存查找内存分配的指针来完成这个工作的。简而言之,如果对于一个内存分配没有一个指针指向它,则它就可以被释放了。这个工作得非常好,但是内存空间越大扫描需要花费的时间越长。
假设你在开发一个内存数据库,或者你在构建一个需要巨大的查找表的数据流水线。在这些场景下,你可能有数个G的内存分配。在这些情形下,你可能会因为GC损失很多的性能。
Is it a big problem?
让我们来看看这个问题到底有多大?下面通过一段很小的代码来演示这个问题。我们分配了10亿(1e9)个8字节的指针,总共占用了8G的内存。然后我强制执行一次GC,并统计GC花费了多少时间。这个过程我们执行了多次来消除误差获得一个比较稳定的数据。在实例代码中,我们还调用了runtime.KeepAlive()
来保证GC或者编译器不会优化掉或回收没有被引用的内存分配。
func main() {
a := make([]*int, 1e9)
for i := 0; i < 10; i++ {
start := time.Now()
runtime.GC()
fmt.Printf("GC took %s\n", time.Since(start))
}
runtime.KeepAlive(a)
}
这段代码的输出如下:
GC took 4.275752421s
GC took 1.465274593s
GC took 652.591348ms
GC took 648.295749ms
GC took 574.027934ms
GC took 560.615987ms
GC took 555.199337ms
GC took 1.071215002s
GC took 544.226187ms
GC took 545.682881ms
可以看到GC占用的时间基本稳定在半秒左右。这里有10亿个指针耶,这有什么值得惊讶的?每个指针所分摊的时间看起来都低于纳秒,这个对于指针查找已经是一个很不错的速度了。
So what next?
这看起来是一个基本原则的问题。假如我们的应用就是需要一个很大的内存查找表,或者我们的应用基本上就是一个很大的内存查找表,那么我们就会遇到这个问题。如果GC以一个固定的时间周期扫描所有的已经分配的内存,我们将会因为GC损失巨大的CPU可用处理能力。对于这种情况我们能做什么呢?
Make our memory dull
如何让内存不被GC盯上?嗯,GC是在查找指针。如果我们分配的对象的类型不包括指针呢,GC还会扫描它们么?
让我们来试试。下面的示例中,我们分配了与前面示例完全相同的内存,但是现在我们没有包含指针类型在里面。我们分配了一个包含了10亿个int类型的数组,这同样占用了8GB的内存。
func main() {
a := make([]int, 1e9)
for i := 0; i < 10; i++ {
start := time.Now()
runtime.GC()
fmt.Printf(&