Go底层原理与工程化实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3.1 GC概述

GC用于回收不再使用的内存。有了GC之后,我们不再需要关注内存的分配与释放。

在介绍GC的基本概念之前,我们先思考一下:垃圾内存的定义应该是什么?如果没有任何途径能访问到这块内存,那这块内存是不是就是垃圾内存了?如何判断有无途径能访问到某块内存呢?有一个经典的方案叫引用计数法:当对象A引用(指针指向)对象B时,对象B的引用计数加1;当删除对象A与对象B之间的引用关系时,对象B的引用计数减1。当某个对象的引用计数为0时,说明没有其他任何对象引用该对象,此时需要回收该对象内存。这有问题吗?想想如果对象A引用了对象B,对象B也引用了对象A,并且没有其他任何对象引用对象A和对象B,对象A和对象B的内存理论上应该是垃圾内存,但是因为对象A和对象B的引用计数都为1,所以都不会被回收。这种情况称为循环引用,也就是说引用计数方案无法处理循环引用的情况。

还有什么其他办法吗?什么对象一定不会被GC回收,访问某对象的途径有什么特点呢?比如栈内存上的对象(随着函数的调用与返回,栈内存自动分配与释放)、全局对象,这两种类型的对象肯定是不能被回收的。GC的目标是堆内存上的对象,并且堆内存上的对象通常需要直接或间接地被栈内存上的对象或全局对象引用(栈内存上的对象、全局对象等因此也被称为根对象),才可以被访问到。那只需要从根对象开始扫描,扫描到的对象肯定就不是垃圾,剩下的没有被扫描到的对象就是垃圾并且需要被回收了。

如图1-2所示,局部变量var1指向堆内存对象A,对象A又指向对象C;全局变量p1指向堆内存对象B,对象B又指向对象E。可以看到,没有任何根对象直接或间接地指向堆内存对象D、F,也就是说无论通过任何途径都无法从根对象访问到对象D、F。最终,堆内存对象D、F将会被GC回收。

图1-2 GC对象示意

上述方案也是三色标记法的基本思路。三色标记法将所有的对象分为已扫描(黑色)、待扫描(灰色)、未扫描(白色)三种类型,而整个GC流程可以简单地划分为标记扫描、标记终止、未启动三个阶段。其中,标记扫描阶段的核心逻辑总结如下:

1)从灰色对象集合中选择一个对象,标记为黑色。

2)扫描该对象指向的所有对象,将其加入灰色对象集合。

3)不断重复步骤1和步骤2。

扫描过程结束后,只会剩下黑色对象与白色对象,而白色对象就是需要回收的垃圾了。显然,标记扫描阶段应该是整个GC流程中最耗时的阶段,因为Go进程通常都会有大量堆内存对象。

最后,GC是需要占用CPU资源的,而Uber则通过GC调优(尽量减少GC)来降低GC对CPU资源的使用,以节约CPU资源。不过在介绍Uber GC调优方案之前,还需要了解GC的触发方式。

Go语言提供了3种GC触发方式:申请内存、定时触发以及手动触发。当你在Go程序中手动调用函数runtime.GC时,就会手动触发GC,注意该函数会阻塞调用方(用户协程)直到GC结束。定时触发也比较简单,每2分钟Go语言会触发一次GC。

申请内存如何触发GC呢?其实只需要在每次申请内存时(参考函数runtime.mallocgc)判断内存使用量是否超过阈值就可以了,如果超过则触发GC。而该阈值是根据环境变量GOGC以及上次GC结束后的内存使用量计算得到的。GOGC的默认值是100,即当内存的使用量达到上次GC结束后的内存使用量的两倍时触发GC。值得一提的是,Uber的GC调优方案其实就是基于GOGC实现的,这一点将在下一小节介绍。

最后补充一下,Go服务触发GC的内存阈值与GOGC以及上次GC结束后的内存使用量有以下关系: