概述
并发回收是指回收器和赋值器可以并发运行的回收算法。以往的算法比如标记整理,复制等都是假定回收时处于STW状态,会暂停其他的线程,而并发算法允许和其他线程同时运行。
三色模型
将对象分为三色
黑:对象连同引用都被扫描过
白:对象未被扫描过
灰:对象被扫描过,引用仍有未被扫描过的
回收过程可以看做是以灰色对象为波面向前推进的过程。灰色对象或者波面将黑色和白色对象分离开。
回收算法正确性
至少要保证只回收垃圾对象,可以不回收完全。
对象丢失问题
前面说过,这里是允许回收器和赋值器并发的,一旦并发,赋值器就可能在回收器执行时修改已经被扫描(标记)过的对象。这样就会影响算法的正确性。下面两个例子:
1)直接删除从灰色到白色的引用
原本Z被Y引用,Y是灰色,最终就可以通过Y扫描到Z,但是期间把Y到Z的引用删除,同时插入了X到Y的引用,由于X已经是黑色,最终导致Z无法被扫描到,Z会被错误的回收。
2)破坏灰色到白色的间接引用
这个就不解释了
从上面的例子可以看到并发回收的难点。
进一步,Wilson[1994]指出:在扫描过程中,只有以下两个条件同时发生时才会有对象丢失问题:
1)赋值器将白色对象的引用插入黑色对象;
2)赋值器破坏了从灰色对象到白色对象的全部路径;
条件一使得这个白色对象一定不是垃圾,条件二使得这个白色对象在之后的扫描中无法被扫描到。所以,这两个条件一起导致了对象丢失问题。
不变式
要想解决对象丢失问题,就得至少打破上面两个条件之一。
弱三色不变式
如果我们想要打破条件二,那么必须让被黑色对象引用的白色对象处于灰色保护状态,灰色保护状态指白色对象直接或者间接被灰色对象引用,即路径没有被打破。
这个被称为弱三色不变式
强三色不变式
如果我们想要打破条件一,很简单,只要保证不存在黑色对象到白色对象的引用。这个被称为强三色不变式。
只要回收时满足了这两个范式,就可以解决对象丢失。很显然,强三色不变一定意味着弱三色不变。
赋值器颜色
回收扫描时,都会从根对象开始。我们可以假象根对象都被赋值器所引用,然后就可以对赋值器做分类。赋值器可以引用黑色,白色或者灰色对象。
黑色赋值器:如果根被扫描过,那么后续不再需要扫描根。
灰色赋值器:不论根是否被扫描过,后续还需要再扫描。
按照这个分类,不同赋值器的根对象颜色会有要求。灰色赋值器由于需要多次扫描,根对象可以是任意颜色的(反正需要多次)。对于黑色赋值器,如果要满足强三色不变式,根对象不能是白色,因为只能扫描一次,如果后面插入了白色引用,那么永远扫描不到。如果要满足弱三色不变式,根对象可以是白色,但必须灰色保护。
讨论这个问题或者赋值器颜色的意义在于:对根对象的颜色和扫描根的次数做了分类。如果允许扫描多次根,那么根对象颜色无所谓。如果只能扫描一次根,那么该白色对象要么不存在要么灰色保护。
新分配对象的颜色
通常新对象没有被任何其他对象引用,可以着为黑色,是一种保守做法。会导致浮动垃圾。
下面介绍解决方案,由于实际的jvm回收器只用了写入屏障技术,这里暂时不讨论读取屏障技术。
基于增量更新的方案
是一种基于强三色不变式的增量更新技术,增量意味这重新扫描。如果一个白色对象被插入到波面之后(即扫描过)的黑色对象中,增量更新方案会把白色对象当做存活对象。需要写入屏障技术拦截这种插入操作。下面看两种写入屏障:
Steele
会把被插入的黑色对象标记为灰色。
Dijkstra
会把插入的白色对象标记为灰色。
二者都着出了一个灰色对象,Steele将黑色对象着为灰色,重新扫描的对象数目对增多,但是精度更高,被插入的白色对象可能会被回收。而Dijkstra将白色对象着为黑色,重新扫描的对象数目对减少,但是原本的白色对象将不会被回收。增量技术由于拦截的白色对象插入到了波面之后的情况,需要重新扫描。因此与灰色赋值器比较搭配。
基于快照的方案
是一种基于弱三色不变式的快照技术,不需要重新扫描。此方案会保证开始可达的对象后面也一定可达。当赋值器删除了灰色到白色的引用后,会将白色对象着色为灰色。后面即使白色对象被插入到黑色对象,也处于灰色保护状态。快照方案修改的是白色对象的颜色,因此是波面之前的,也就不需要重新扫描,保证的是在后续的扫描中可以扫到。快照方案只能和黑色赋值器搭配。因为屏障技术无法追踪根对象,一旦不处于灰色保护的白色根先被黑色对象引用,后面又从赋值器删除,那么无法被扫描到。