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

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

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

垃圾回收算法的性能点

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

  • 吞吐量:回收固定内存需要的时间
  • 最大暂停时间:回收过程中需要暂停代码执行的时间
  • 内存使用效率:真正用于逻辑的内存占总内存的比例
  • 访问的局部性:与计算机各项缓存的友好程度

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

基本 GC 算法

1. 标记-清除

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

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

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

  • 对象的内存分配和对象的内存回收策略
  • 从根对象开始标记对象的方式

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

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

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

长此以往,我们会发现内存就会有一个一个的洞,碎片化会很严重,导致内存的利用率逐渐下降。同时,因为这里的内存是一块一块的,所以我们用链表来保存它的时候,分配内存查找又是一个问题,所以就很麻烦。

此外,周期性得标记对象,从而会周期性得改变对象的微小数据,所以导致操作系统 COW 体系不能得到较好的运用,从而导致性能的缺失。这是一方面,前面还有一个问题,那就是我们标记对象的时候以怎么样的顺序来查找活动对象,常见的查找方式有深度优先查找广度优先查找,这两种查找在性能上可能没有太大区别,但是,对于临时空间的占用却是有较大的影响,所以一般来说,深度优先广度优先更能压低内存使用量,所以经常使用的是深度优先搜索

虽然有缺点,但是标记-清除的优点也是比较明显的,例如实现起来还是比较简单的,与保守式 GC 是兼容的,使得 标记-清除 算法在实际应用中还是得到大家的青睐的。

2. 引用计数

除了标记-清除算法外,引用计数 也是一种不错的方法,引用计数算法 顾名思义就是在对象中额外记录自身被引用的次数,当次数减小到 0 的时候那么就知道自己已经没有用处了,可以被回收了。也是一种很简单很直观的方式,可以在对象不被使用的时候立刻回收掉内存,从而将垃圾回收的时间分散化,也不需要像 标记-清除 一样需要进行遍历查找。

但是这也带来了一定程度的麻烦,例如,我们需要使用内存屏障管理引用计数,对象的生成、赋值和引用都涉及引用计数的变化,从而导致引用计数的增减处理频繁;同时,因为引用计数的存在,我们还需要在对象的自身数据之外,为引用计数分配固定的空间来存放计数,这是固有损耗。还有一个致命的缺点就是,使用引用计数算法,无法清除 循环引用 的问题,从而导致内存一直占用,无法释放。

3. GC 复制

前面介绍的两种方法都是在对象本身上操作的,也就是说清除和释放都是操作对象本身所在的位置,但是,GC 复制算法 就稍微复杂一些了,GC 复制算法 最原始的做法就是将内存一分为二,每次只使用其他一半,当要 GC 的时候就将使用着的一半中的活动对象复制到另外一半中,然后清理掉这一半中的所有对象,直接使用另外一半即可,重复这个操作。

这个我们一眼就可以看出问题,那就是空间的利用率不高,但是,好处也是非常明显的,首先是速度快,没有额外的标记-清理操作,就是直接的复制,高吞吐;分配对象直接分配,不需要考虑碎片化问题;还可以保持与 OS 的缓存兼容,优势还是比较明显的。然而,硬币总有正反面,除了空间利用率不高之外,这种方法不兼容保守式的 GC 算法,此外,对于递归调用还会有栈溢出的风险。

所以为了更好得完善了这个算法,还有有很多改进思路被提出的,例如不是将空间划分为两部分,而是划分为多个部分,从而提升空间的利用率就是其中的一个思路。

总结

本文就常见的三种垃圾回收基本算法以及经常需要考虑的几个性能指标进行介绍,从而为了解垃圾回收开一个头。其实看各种编程语言的 GC 实现都会发现本文中基本算法的身影,无非就是它们直接如何组合,所以,理解本文中的基本算法对于理解其他编程语言的 GC 实现还是很有帮助的。

Reference

  1. Garbage Collection

垃圾回收(GC) 的基本算法的更多相关文章

  1. 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法

    垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己 ...

  2. 从C#垃圾回收(GC)机制中挖掘性能优化方案

    GC,Garbage Collect,中文意思就是垃圾回收,指的是系统中的内存的分配和回收管理.其对系统性能的影响是不可小觑的.今天就来说一下关于GC优化的东西,这里并不着重说概念和理论,主要说一些实 ...

  3. 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配

    垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己 ...

  4. Java 垃圾回收(GC) 泛读

    Java 垃圾回收(GC) 泛读 文章地址:https://segmentfault.com/a/1190000008922319 0. 序言 带着问题去看待 垃圾回收(GC) 会比较好,一般来说主要 ...

  5. 性能测试三十五:jvm垃圾回收-GC

    垃圾回收-GC 三个问题 哪些内存需要回收? 什么时候回收? 如何回收? YoungGC和FullGC: 新生代引发的GC叫YoungGC 老年代引发的GC叫FullGC FullGC会引起整个Jvm ...

  6. JVM虚拟机(四):JVM 垃圾回收机制概念及其算法

    垃圾回收概念和其算法 谈到垃圾回收(Garbage Collection)GC,需要先澄清什么是垃圾,类比日常生活中的垃圾,我们会把他们丢入垃圾箱,然后倒掉.GC中的垃圾,特指存于内存中.不会再被使用 ...

  7. 垃圾回收GC:.Net自己主动内存管理 上(三)终结器

    垃圾回收GC:.Net自己主动内存管理 上(三)终结器 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主 ...

  8. JVM学习——垃圾回收GC(学习过程)

    JVM学习-垃圾回收(GC) 2020年02月19日06:03:56,开始学习垃圾回收,学习资料来源(张龙老师的JVM课程) JVM内存数据区域知识复习 学习垃圾回收之前,要对JVM内部的内存区域有详 ...

  9. 类装饰器,元类,垃圾回收GC,内建属性、内建方法,集合,functools模块,常见模块

    '''''''''类装饰器'''class Test(): def __init__(self,func): print('---初始化---') print('func name is %s'%fu ...

  10. 修改Tomcat的jvm的垃圾回收GC方式为CMS

    修改Tomcat的jvm的垃圾回收GC方式 cp $TOMCAT_HOME/bin/catalina.sh $TOMCAT_HOME/bin/catalina.sh.bak_20170815 vi $ ...

随机推荐

  1. Struts2学习笔记整理(四)

    Struts2上传下载 文件上传 如果想使用HTML表单上传文件(一个或多个),那么必须把HTML表单的enctype属性设置成multipart/form-data,且method=post, 且使 ...

  2. 后台程序处理 (一)python asyncio 协程使用

    由于脚本需要在完成事件处理后N秒检查事件处理结果,当执行失败时再执行另一个事件处理. 想要最小化完成这个功能.同时在第一时间就将执行完毕的结果反馈给接口. 因此想到使用协程. 使用之前先翻阅了一下现有 ...

  3. .net 裁剪图片(不压缩)

    命名空间: using System.Drawing; using System.Drawing.Imaging; /// <summary> /// 生成图片缩略文件 /// </ ...

  4. C#设计模式之二十二访问者模式(Visitor Pattern)【行为型】

    一.引言   今天我们开始讲"行为型"设计模式的第九个模式,该模式是[访问者模式],英文名称是:Visitor Pattern.如果按老规矩,先从名称上来看看这个模式,我根本不能获 ...

  5. sqlserver 存储过程 游标实例

    if exists(select * from sysobjects where id = object_id(N'dbo.test_cursor') and type = 'P') drop PRO ...

  6. GitLab配置ssh key

    一.背景 当前很多公司都选择git作为代码版本控制工具,然后自己公司搭建私有的gitlab来管理代码,我们在clone代码的时候可以选择http协议,当然我们亦可以选择ssh协议来拉取代码.但是网上很 ...

  7. bzoj 1597: [Usaco2008 Mar]土地购买

    Description 农 夫John准备扩大他的农场,他正在考虑N (1 <= N <= 50,000) 块长方形的土地. 每块土地的长宽满足(1 <= 宽 <= 1,000 ...

  8. 根据NPOI 读取一个excel 文件的多个Sheet

    大家都知道NPOI组件可以再你本地没有安装office的情况下来 读取,创建excel文件.但是大家一般都是只默认读取一个excel文件的第一个sheet.那么如果要读取一个excel 的所有shee ...

  9. FPGA设计思想与技巧(转载)

    题记:这个笔记不是特权同学自己整理的,特权同学只是对这个笔记做了一下完善,也忘了是从那DOWNLOAD来的,首先对整理者表示感谢.这些知识点确实都很实用,这些设计思想或者也可以说是经验吧,是很值得每一 ...

  10. django xdmin使用

    我们来看看我们原先django给我们自带的admin后台是什么样子的呢 有人说,你的界面怎么那么丑,我说这个还叫丑吗,他说丑,我说你来,我看看你的,上图 看到登录界面后,我说别看了,我去修改,修改,我 ...