R循环有两个_C++垃圾回收器:解决循环引用问题

本篇讨论一个核心问题,如何在一大堆复杂的引用关系中找到循环引用的对象。引用计数无法解决这个问题,选择了用shared_ptr,weak_ptr来回避这个问题。mark and sweep通过给每个节点打标签的方式来判断哪些节点是可达,不可达的,没有去作循环检测。这个两种GC算法都选择避开循环引用问题来实现GC。而作者实现的GC算法,是通过判断节点是否存在循环引用来实现GC。下面就来看看如何去检查循环引用的:

首先,明确一下检测循环引用的时间成本和mark and sweep一致,都是遍历一次就可以完成。不同的是mark and sweep在遍历的过程仅仅是为节点打标签(mark),而检测循环引用还需要一些其他步骤,并且这些步骤时间成本几乎可以忽略不计。

作者在网上搜索关于GC方面的内容时,发现还有人和作者一样,也是用智能指针加循环检测的方式实现了GC,并且他的循环引用方法非常直观和简单。这里给出github地址,有兴趣的朋友可以去看看:Better-Idea/Mix-C

我们先来看看他的循环检测的方法,理论上就这一句:

对于一个闭合有向图出度和等于入度和

入度:以顶点为终点的有向边的数量为入度。

出度:以顶点为起点的有向边的数量为出度。

这是图论的一条基本概念,具体怎么理解,画个图就明白了,见图:

983218c13abec3860a7d09ef30d1bd00.png
出度和:4 入度和:4

上图是一个封闭的简单有向的矩形,遍历一遍,计算出所有顶点的入度,出度。因为上图的矩形是一个闭合的图形,所以它的所有顶点的出度和入度的总和是相等的。例如上图入度总和是4,出度总和也是4。并且把矩形边的指向转换一下也是成立的。如此,通过一次遍历我们就能判断哪个节点是存在循坏引用的。

上文提到的方法虽然简单,效率高,但有个缺陷。进行循环检测的时候只能判断单个顶点是否存在循坏引用,例如上图四个顶点可以看作是一体,它们四个一同构成了循环。如果该方法运用到GC算法中,那就会导致大量重复检测的问题。

虽然都是利用图论来检测循环引用,但作者的原理和他不相同,作者的利用tarjan算法,可以一次找出全部构成循环的顶点,极大的减少了检测的次数。在图论中,像上图一样构成互相循环的顶点有一个明确的名字和概念:强连通分量。

关于tarjan算法和强连通分量相关的知识,可以看这篇文章:有向图强连通分量的Tarjan算法

这里就不多介绍了。

下面来看看作者循坏检测是怎么做的(准确的说应该是判断对象有没有失去引用):

首先在原来引用计数的基础上,在增加一个循坏计数,循坏计数表示在进行遍历过程中重复访问的次数。例如下图:

48ee97cdce8df620a82775cc62098b43.png
箭头表示指针,圆圈表示需要遍历的顶点

一个对象被两个指针指着,其中一个指针形成回路,假如对它进行遍历,从顶点出发一次就回到了原点,所以它的引用计数是2,循坏计数是1。上图很直观,虽然有图中存在回路,但由于外部还有一个指针指着,所以该对象续存。确切的说,因为对象的引用计数大于循环计数所以续存,另外的,引用计数一定大于等于循环计数,引用计数为零的情况下,对像一定是失去引用的。我们重点需要解决的是引用计数等于循环计数的情况。

我们来看一个复杂点的情况,如图:

edaea1e958131aa0d03f0bacd4e0d03c.png
方框内:r表示引用计数,c表示循坏计数。 pa, pb是两个独立的指针,并不属于任何顶点。

上图是由从顶点A开始遍历得到的。假设我们把pa赋空,从而A点的引用数据为r:2,c:2,在引用计数循环计数相同的情况下,不能简单断定A对象失去引用。如图中的E点,E点还有被pb引用。由于A,D,E,B构成一个循坏(强连通分量,强连通分量怎么得到看tarjan算法),因为E点存在外部引用,所以整个强连通分量内的顶点都不能释放,反之,整个连通分量都需要释放。

那么如何判断强连通分量内的顶点是存在外部引用呢?很简单,首先,通过tarjan算法得到了强连通分量,依次比较每个顶点的引用计数和循环计数,如果引用计数减去循环计数大于一即存在外部引用,找到一个这样节点就不用继续在做比较,即整个强连通分量都续存。如图上的E点r - c 等于 2,大于1,所以A点续存,整个图都续存。

反之,A,B,E,D都是垃圾节点,都需要释放,连带的当A,B,E,D都释放后,C点的引用计数为零,随后也会释放。

利用tarjan算法可以批量的找到垃圾节点,并保证全局每个节点只遍历一次。整个GC算法基本就是在tarjan算法的基础上添加了计数改进而成。tarjan算法的时间复杂度为O(N+M),也是很高效了。

至此就是作者的GC算法了。

然后说一些题外话:很多人说我的GC算法没有涉及到内存分配,碎片整理之类的内容,没啥实际价值。其实GC算法本身就是和内存无关的,更多的是和图论相关。mark and sweep,reference count基础理论都是不涉及内存方面的,内存方面的问题应该归类为memory allocator的问题。

为什么很多人把GC和memory allocator这两个问题捆绑到一起呢?因为业界主流的GC都是把两者捆绑在一起实现的。例如 java,C#等语言。这其中有出于性能,实现方便等考量,但还有一个重要原因: java,C#中GC是做为基础设施出现的,(很难想象java中可以自己实现allocator的场景)allocator方面的工作也基本交给语言了。但是C++不一样,C++不会限制使用者的使用方式,没有条条框框,没有框架。用户想用其他方式管理内存,就可以用其他方式管理内存,例如所有的STL容器都支持自定义allocator。如果一个GC算法它一定要求用户需要按照它的框架来编码,来管理。那么这个GC算法是不会被STL所接纳的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值