lua的GC采用的是Mark&Sweep算法。根据之前的介绍可知,Mark&Sweep算法它是一种需要Stop the world的算法,其GC操作是不可打断的,那下面首先来分析一下为何会不可打断并且在lua中是如何解决这个问题的:
把不可达对象看做白色,可达对象看做黑色,那Mark&Sweep算法的伪代码如下:
每个新创建的对象的颜色为白色
//初始化阶段
遍历root链表中的对象,并将其加入到对象链表中
//标记阶段
当对象链表中还有未扫描的元素:
从中取出一个对象并将其标记为黑色
遍历这个对象关联的其他所有对象:
标记为黑色
//回收阶段
遍历所有对象
如果为白色:
这些对象就是没有被引用的对象,逐个回收
否则:
这些对象是被引用的对象,重新加入对象链表中等待下一轮的GC检查
图示如下:
下面分析这个过程不能被打断的原因:如果在遍历对象链表时标记每个对象颜色的过程中被打断,此时新增了一个对象,那么应该将这个对象标记为白色还是黑色?如果标记为白色,假如GC已经到了回收阶段,那么这个对象就会在没有遍历其关联对象的情况下被回收;如果标记为黑色,假如GC已经到了回收阶段,那么这个对象在本轮GC中并没有被扫描过就认为是不必回收的。可以看到,标记阶段和回收阶段必须合在一起完成。
于是lua5.1版本引入“三色增量标记清除算法”,实现了增量式GC。而它的三种颜色分类如下:
1.白色:当前对象为待访问状态,表示对象还没有被GC标记过,这也是任何一个对象创建后的初始状态。换言之,如果一个对象在结束GC扫描过程后仍然是白色,则说明该对象没有被系统中的任何一个对象所引用,可以回收其空间了。
2.灰色:当前对象为待扫描状态,表示对象已经被GC访问过,但是该对象引用的其他对象还没有被访问到。
3.黑色:当前对象为己扫描状态,表示对象已经被GC访问过,并且该对象引用的其他对象也被访问过了。
将这几个过程的操作和颜色的切换结合起来,如下图所示:
lua5.1GC算法伪代码:
每个新创建的对象颜色为白色
//初始化阶段
遍历root节点中引用的对象,从白色置为灰色,并且放入到灰色节点列表中
//标记阶段
当灰色链表中还有未扫描的元素:
从中取出一个对象并将其标记为黑色
遍历这个对象关联的其他所有对象:
如果是白色:
标记为灰色,加入灰色链表中
//回收阶段
遍历所有对象:
如果为白色:
这些对象都是没有被引用的对象,逐个回收
否则:
重新加入对象链求中等待丁一轮的GC检查
引入了灰色节点的概念后,算法不再要求一次性完整执行完毕,而是可以把已经扫描但是其引用的对象还未被扫描的对象置为灰色。在标记阶段中,只要灰色节点集合中还有元素在,那么这个标记过程就会继续下去,即使中间被打断转而执行其他操作了,也没有关系。
即使是这样,却仍然有另一个没有解决的问题。从上面的算法可以看出,没有被引用的对象的颜色在扫描过程中始终保持不变,为白色。那么,假如一个对象在GC过程的标记阶段之后创建,根据前面对颜色的描述,它应该是白色的,这样在紧跟着的回收阶段,这个对象就会在没有被扫描标记的情况下被认为是没有被引用的对象而删除。
因此,Lua的GC算法除了前面的三色概念之外,又细分出来一个“双白色”的概念。简单地说,Lua中的白色分为“当前白色”(currentwhite )和“非当前白色”(otherwhite)。这两种白色的状态交替使用,第N次GC使用的是第一种白色,那么下一次就是另外一种,以此类推。
代码在回收时会做判断,如果某个对象的白色不是此次GC使用的白色状态,那么将不会认为是没有被引用的对象而回收,这样的白色对象将留在下一次GC中进行扫描,因为在下一次GC中上一次幸免的白色将成为这次的回收颜色。
此外,上面的图示中还有两条线:barrier fwd和barrier back。我们知道,增量式的MarkSweep算法的执行中是可以被打断以执行其他操作的,此时就会出现新增加的对象与已经被扫描过的对象之间会有引用关系的变化,而算法中需要保证不会出现黑色对象引用的对象中有白色对象的情况,于是需要两种不同的处理:
barrier fwd(标记过程向前走一步) :如果一个新创建对象的颜色是白色,而它被一个黑色对象引用了,那么将这个对象的颜色从白色变成灰色,也就是这个GC过程中的进度向前走了一步。
barrier back(标记过程向后走一步) :如果一个新创建对象的颜色是白色,而它被一个黑色对象引用了,将黑色的对象回退到灰色,也就是这个原先已经被标记为黑色的对象需要重新被扫描,这相当于在GC过程中向后走了一步。
对于这两种处理,barrier back操作仅针对Table类型的对象,而其他类型的对象都是向前操作。
下面分析一下原因:
table是lua最常见的数据结构,而一个table与key、value之间是1:N的对应关系,如果table做barrier fwd操作,就意味着:只要一个table中有新增对象,那么这个新增对象就需要标记为灰色加入到灰色链表等待扫描,而如果执行barrier back操作则只需要把该table对象回退到灰色状态即可,减少了不必要的开销。
但是执行barrier back操作时要注意的是,对table对象执行了此操作后并不是放入灰色链表中,而是放入了一个grayagain的链表中,当灰色链表中没有元素了以后才会处理grayagain链表并且是一次性处理完,处理完以后结束标记阶段。
综上,就是lua5.1版本实现的“三色增量标记清除算法”的原理了。在lua5.2版本,试验性的实现了分代GC,但是由于其实现确实有设计问题,没有收到太多正面反馈,所以在5.3版本中移除。而5.4版本又重新设计实现了分代GC,目前网上没有相关资料,感兴趣的可以阅读源码。