垃圾回收器的相关知识点总结(2)
在这个算法的执行过程中,我们始终维护两个出区中的指针:allocationPtr指向我们即将为新对象分配内存的地方,scanPtr指向我们即将进行活跃检查的下一个对象。scanPtr所指向地址之前的对象是处理过的对象,它们及其邻接都在出区,其指针都是更新过的,位于scanPtr和allocationPtr之间的对象,会被复制至出区,但这些对象内部所包含的指针如果指向入区中的对象,则这些入区中的对象不会被复制。逻辑上,你可以将scanPtr和allocationPtr之间的对象想象为一个广度优先搜索用到的对象队列。
译注:广度优先搜索中,通常会将节点从队列头部取出并展开,将展开得到的子节点存入队列末端,周而复始进行。这一过程与更新两个指针间对象的过程相似。
我们在算法的初始时,复制新区所有可从根对象达到的对象,之后进入一个大的循环。在循环的每一轮,我们都会从队列中删除一个对象,也就是对scanPtr增量,然后跟踪访问对象内部的指针。如果指针并不指向入区,则不管它,因为它必然指向老生区,而这就不是我们的目标了。而如果指针指向入区中某个对象,但我们还没有复制(未设置转发地址),则将这个对象复制至出区,即增加到我们队列的末端,同时也就是对allocationPtr增量。这时我们还会将一个转发地址存至出区对象的首字,替换掉Map指针。这个转发地址就是对象复制后所存放的地址。垃圾回收器可以轻易将转发地址与Map指针分清,因为Map指针经过了标记,而这个地址则未标记。如果我们发现一个指针,而其指向的对象已经复制过了(设置过转发地址),我们就把这个指针更新为转发地址,然后打上标记。
算法在所有对象都处理完毕时终止(即scanPtr和allocationPtr相遇)。这时入区的内容都可视为垃圾,可能会在未来释放或重用。
秘密武器:写屏障
上面有一个细节被忽略了:如果新生区中某个对象,只有一个指向它的指针,而这个指针恰好是在老生区的对象当中,我们如何才能知道新生区中那个对象是活跃的呢?显然我们并不希望将老生区再遍历一次,因为老生区中的对象很多,这样做一次消耗太大。
为了解决这个问题,实际上在写缓冲区中有一个列表,列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障——因为每个写操作都要经历这样一关。
你可能好奇,如果每次进行写操作都要经过写屏障,岂不是会多出大量的代码么?没错,这就是我们这种垃圾回收机制的代价之一。但情况没你想象的那么严重,写操作毕竟比读操作要少。某些垃圾回收算法(不是V8的)会采用读屏障,而这需要硬件来辅助才能保证一个较低的消耗。V8也有一些优化来降低写屏障带来的消耗: