垃圾回收(GC) 的基本算法

GC 作为一个长久的话题,从诞生[1]至今也算是经历了六七十年了,对于很多习惯于使用 Java/Python 的同学来说,对于内存的管理可能会稍微更陌生一些,因为这些语言在语言层面就屏蔽了内存的分配和管理,帮助我们减少了超多的麻烦。但是,在帮助我们减少麻烦的同时,也带来了很多问题,其中一个就是内存爆掉,这个问题有可能是代码写得不好,有可能是设计不好,反正就是存在这个问题。

本文不准备细究这些问题,本文旨在介绍一些内存回收的基本算法,通过这些基本算法,从而介绍一下这些自动内存管理语言底层管理内存的一些套路,从而在平时使用它们的时候可以依照它们的尿性来编写代码,减少一些内存管理方面的 Bug。

反观这么多年来,GC 虽然发展了这么久,从古老的 Lisp 到新一些的 Go 语言,垃圾回收的基本算法都没有太大的创新,一方面说明了这些算法的强大,另外一方面也说明了这里还有很大的挖掘空间给爱好者们/专家们去思考,挖掘出新的基本算法。本文就对这些年一直被各种编程语言直接使用/配合使用的几种垃圾回收算法进行一个总结介绍,顺便介绍一下他们的优缺点。

垃圾回收算法的性能点

为什么会存在那么多的垃圾回收算法呢?我想这个问题的答案可能是没有任何一种内存回收算法是完美的,所以在针对不同的情景需求下,不同的内存回收算法有其独特的优势,所以最后就延续了多种回收算法。那么,在平时的大多数情况下,有哪些性能考虑点是我们关注的呢,下面就列举一下常见的性能指标

吞吐量:回收固定内存需要的时间

最大暂停时间:回收过程中需要暂停代码执行的时间

内存使用效率:真正用于逻辑的内存占总内存的比例

访问的局部性:与计算机各项缓存的友好程度

虽然这不是所有的关注指标,但是这些却是大部分情况下被关注的指标。而且,需要注意的是,这里面有一些指标是互斥的,例如我们会发现,最大吞吐量和最大暂停时间往往无法得到双赢,也就是说无法同时满足这两项的最优。所以,在选择具体的回收算法的时候,其实就是在这些指标之间进行权衡,然后根据自己的需求进行选择。下面就对常见的三种基本回收算法进行介绍。

基本 GC 算法 1. 标记-清除

标记-清除算法是一个比较经典的算法了,在标记-清除算法中,一般都是有所谓的根对象,而且一般来说根对象都不止一个,有很多,以 C 语言来理解的话,我们可以理解成分配在栈中的对象和全局对象都是所谓的根对象。标记-清除算法从这些所谓的 根对象 出发,进行第一个阶段——标记阶段,也就是将这些 根对象 能够引用到的那些对象都作上标记,一般的做法是每个对象都有一个字段用于标识是否被标记,当然还有很多其他的做法,例如专门弄一张表来表示对象的标记等,这些都是后话啦,反正这个阶段就只做一件事情,那就是找出被使用的对象,作上标记,这样没有被标记的对象也就是不用的对象了。

在第一阶段标记完之后,那么进入标记-清除的第二个阶段——清除阶段,清除阶段其实也就是所谓的释放阶段,无非就是把不使用的对象所占用的内存释放掉,然后回收起来这么简单。

看上去标记-清除算法还是比较简单的,但是,这个简单背后也是有很多需要思考的问题:

对象的内存分配和对象的内存回收策略

从根对象开始标记对象的方式

这是两个比较常见的问题。第一个问题,对象的内存分配问题,假设现在我们的语言需要创建一个对象,那么自然需要分配一块内存给它,怎么分配这个内存呢?一个可能的做法就是从上次分配的位置往后直接分配一块,这样保证每次分配的内存都是往高位走,内存地址逐渐叠加。但是,这种方法带来了一个问题,那就是释放的时候就很尴尬了:

垃圾回收(GC) 的基本算法

假设这里有一段内存,按照刚才的策略分配了 A、B 和 C 三个对象,当程序运行一段时间之后,我们想回收掉对象 B,然后回收之后发现现在的内存是这样的:

垃圾回收(GC) 的基本算法

这个时候,我们想再分配一个对象 D,那么不巧,D 的大小就比 B 大那么一点,所以原来 B 的位置不足以容纳 D,所以也就不能使用 B 原来的位置,那么这样的话,内存结构可能就成了这样:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpxfzg.html