高质量编程与性能调优实战:垃圾回收概述-以Go为例(1)| 青训营

349 阅读10分钟

青训营笔记,在这篇文章中我们主要探讨一下我们将代码写好后,编译器是怎么处理优化我们的代码的(勉强算得上是切题了QAQ)。本次主要记录垃圾回收的概念、以及引用计数、串行标记-清除、并发标记-清除以及三色标记法。

0.为什么需要垃圾回收:

 整体而言,垃圾回收可以被归纳一种资源管理策略,为在平时编写程序的过程中,我们都会创建变量或者对象,在这个过程中,程序员或者编译器会在虚拟内存中划分一块区域。而当这个对象或者变量不再被使用后,在未经处理的情况下其依然会留存在内存当中,这个时候这个变量就成为了“垃圾”。其主要影响可以被归纳为两个方面

  1. 浪费虚拟内存空间:这个很好理解,如果垃圾不被回收,虚拟内存中就会被这些的不再被使用的类和变量给堆满,影响真正有用的变量和对象
  2. 为长时间执行的程序带来风险:对于一些需要长时间运行的程序,在使用完内存资源后如果没有及时释放内存资源,容易造成内存泄露,比较经典的例子就是如下的野指针:
swap( int* p1,int* p2 )
{
 int *p;
 *p = *p1;
 *p1 = *p2;
 *p2 = *p;
}

 当swap函数被多次调用的时候,就非常容易导致内存泄露,引起系统崩溃

 而针对这一问题,我们需要引入垃圾回收机制,严格意义上来说,垃圾回收并不属于编译器的主营业务,操作系统一般会记录一个进程运行时所占的资源(内存、cpu、寄存器等),进程一旦结束,操作系统就会自动释放这一部分资源。

 但是这种方案并无法适配平时实际程序的应用场景,对于某些程序而言,其需要长时间进行运行,依赖传统的操作系统主导的垃圾回收明显是不行的,于是我们需要另外的一套算法来进行垃圾回收。

1.垃圾回收分类:

 垃圾回收算法有非常多,本文主要基本的本文主要围绕Go的几个比较经典的垃圾回收方法进行介绍。本次主要介绍串行标记-清扫, 并发标记清扫,三色标记法这三种。

Go版本号垃圾回收算法
v1.1串行标记-清扫
v1.3并发标记清扫
v1.5三色标记法

2 算法介绍

2.1 引用计数:

2.1.1 引用计数的原理与实现:

 引用计数的本质是通过对资源的被引次数进行管理,从而达到资源使用效率最大化的目的。具体的实现是通过每一个单元去维护一个域,保存其他单元指向他的引用数量。当这个数量为0的时候,就对这块资源进行回收。  这里我们给出一个比较直观的例子,参见以下这个图,我们一共实例化了三个对象A,B,C,每个对象的计数器的初始值均为0。

image.png  其中B、C分别在执行过程中对A进行了引用(此处表示为箭头指向A)那么此处A的计数器就会+2。如果之后B不再引用A了,那么就会将此时的计数器数量-1,如果,C,B均不再应用A。此时计数器就会被清0,对象A就会被进行回收。在具体实现上,对象会将自己作为空闲空间链接到链表。

 总结一下,引用计数的步骤主要可以被分为以下三个重点

  • 确定资源被引状况
  • 每多一个引用,计数+1;每少一个引用;计数-1
  • 当计数器为0的时候,对对象进行回收

2.1.2 引用计数的优势与劣势:

引用计数这种方法的主要优势有以下几个:

  1. 不需挂起程序:对于一些GC策略,其需要在一段时间内将程序挂起,单独进行垃圾回收。而引用计数在整个程序执行的期间内都可以进行垃圾回收。
  2. 实时性强:引用计数的算法实现的时候,每个耗尽对会有计数器,一旦为0就可以即刻回收,不需要堆上的资源耗尽后才能判断某个对象或者变量是否是垃圾。
  3. 算法相对简单(当然实现上并不简单)

引用计数这种方法的劣势也相对明显:

  1. 无法处理循环引用:循环引用的情况如下所示:

image.png 循环引用的经典出现场景是两个对象是同一个类的实例,同时属性之间相互赋值,再这种情况下,无法有效减少计数器数量

  1. 额外资源开销:引用计数法本身需要一个额外的计数器,故此需要额外的资源,同时针对计数器的增减更新也需要消耗额外的资源。

2.2 串行标记-清扫

2.2.1 标记-清扫的原理与实现

 标记清扫可以算的上是最为古老的垃圾回收算法了,其主要是通过间接的方式进行回收。在这个算法当中,其会对象进行扫描(也被称为追踪,这也是为什么标记-清扫是被称为追踪式算法),标记扫描到的对象,而剩下的对象就会被视为是垃圾, 进行回收。整体上而言,标记-清扫算法主要可以分为以下几个步骤:

  1. 挂起程序,进行垃圾回收(这个地方也被称为Stop The World(STW))
  2. 标记:从根对象进行扫描,并且进行标记
  3. 清扫:将没有被标记的垃圾进行回收
  4. 垃圾回收过程结束,唤醒程序  具体我们可以来看看以下这个例子,这是执行垃圾清扫前的程序的情况

image.png  此时我们开始执行标记,此处红框的地方代表被标记的对象,此处我们分别标记了A,B,C这三个对象,其中没有引用的对象D为被标记,就需要被进行回收。

image.png

2.2.3 串行标记-清扫的优势与劣势:

 标记清扫的优势非常明显,此处我们简单概括一下

  1. 避免了循环引用的情况发生
  2. 程序设计相对简单

 于此同时,标记清扫算法也有一定的劣势,我们也对其进行概括。

  1. 需要进行STW:串行标记-清扫的方式中,程序需要先被挂起后,才能进行进行GC。
  2. 容易产生碎片:标记-清扫的方法并不会考虑内存中各个对象的位置,在进行一次垃圾回收后,如果后期新建的对象较大,则需要再次进行一次垃圾回收。具体可以看看下面这个情况

image.png

 此时我们对垃圾进行一次回收:

image.png  此时,如果我们需要实例化一个占15个内存格的对象,那么此时就需要再次进行一次GC,否则不够容纳新的对象。

2.3 并发标记-清扫:

2.3.1 并发标记-清扫的原理与实现

 并行垃圾清扫实际上是串行垃圾清扫的一种改版,其主要思想是通过调整GC的步骤顺序来对优化垃圾回收的效率,此处我们通过下面两张图来进一步说明

image.png

image.png  从上面两张图中,我们可以发现,在第一张图所标示的并发标记-清扫算法中,程序在标记后会被唤醒、继续执行,在程序执行的通同时完成清扫任务,这样就比起传统的传统的串行标记-清扫算法,其效率会更高。

 我看到这里的时候有一个疑问,既然要通过并发来优化,为什么不可以一边执行程序,一边执行标记和清除,这样更加彻底,而需要保留STW呢?这么做的依据在于程序执行端时候进行标记-清除,可以确保程序在进行GC之间没有任何新的对象产生。如果不引入STW而直接执行标记-清除,会导致运行期间程序产生的新对象没有被标记到,而被错误回收。

2.4 三色标记法:

2.4.1 三色标记法的原理与实现

 三色标记法在某种程度上来说算是标记-清扫算法的一种优化,在我们上面看的的两种算法中,最大的不足之一是为了垃圾回收的有效性必须引入STW机制,让程序长时间挂起。三色标记法通过引入着色的方式,使得垃圾回收可以被异步执行。其中三色标记法中的白、灰、黑三种颜色分别代表以下三个状态: 1.白色:尚未被GC标记过的对象,如果全部对象标记完成,则为垃圾对象 2. 灰色:本对象已经被GC标记过,但是其子引用仍然未被GC标记过,属于中间态。

 三色标记法的步骤大约分为以下三个。

  1. 检查对象:检查出需要进行标记的对象。
  2. 进行初始着色:将所有对象都设置为白色
  3. 开始进行着色,此时可以先将所有的全局变量和函数栈中的对象设置为灰色。
  4. 按照引用访问对象的子引用,如果当前对象不存在子引用,就将其设为黑色,反之如果对象存在子引用,则将所有的子引用都设为灰色,当前对象设为黑色。
  5. 重复第四步

 具体的实现,我们可以看看以下这个例子

 先将所有对象都设为白色。

image.png  所有的全局变量和函数栈中的对象设置为灰色。 image.png  按照引用访问对象的子引用,如果当前对象不存在子引用,就将其设为黑色,反之如果对象存在子引用,则将所有的子引用都设为灰色,当前对象设为黑色。 image.png  重复上面的步骤

image.png  再次重复,此时已经没有灰色了,回收垃圾对象B,C,H

image.png

2.4.2 三色优化的优缺点:

 三色优化的优点非常明显,就是其高实时性,其可以以中断时间极少的代价或者完全没有中断来进行整个GC。

 三色优化的劣势主要在于,当运用于与用户线程并发运行的情况时,对象的引用并不稳定,其可能会发生改变,造成多标或者漏标。针对这个情况的优化我会在下一篇博文进行阐述。

下次的博文主要记录介绍在Go中的垃圾回收算法的具体运行流程

参考文献

  1. golang 垃圾回收 - 知乎 (zhihu.com)
  2. Golang 垃圾回收机制 - hezhixiong - 博客园 (cnblogs.com)
  3. 面试官:并发标记时如何标记垃圾 & 垃圾回收之三色标记法详解_青蓝饮墨的博客-CSDN博客
  4. 三色标记法与读写屏障 - 简书 (jianshu.com)
  5. 深入理解GO语言垃圾回收全流程_go 垃圾回收_量子学习法的博客-CSDN博客
  6. 四种垃圾回收算法(标记-清除算法【Mark-Sweep】,复制算法【Coping】,标记-整理算法【Mark-Compact】,分代收集算法【Generational Collection】) - 晶晶 - 博客园 (cnblogs.com)