2012年年中,Google引入了两项改进来减少垃圾回收所引起的停顿,并且效果显著:增量标记和惰性清理。
增量标记允许堆的标记发生在几次5-10毫秒(移动设备)的小停顿中。增量标记在堆的大小达到一定的阈值时启用,启用之后每当一定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。就像普通的标记一样,增量标记也是一个深度优先搜索,并同样采用白灰黑机制来分类对象。
但增量标记和普通标记不同的是,对象的图谱关系可能发生变化!我们需要特别注意的是,那些从黑对象指向白对象的新指针。回忆一下,黑对象表示其已完全被垃圾回收器扫描,并不会再进行二次扫描。因此如果有“黑→白”这样的指针出现,我们就有可能将那个白对象漏掉,错当死对象处理掉。(译注:标记过程结束后剩余的白对象都被认为是死对象。)于是我们不得不再度启用写屏障。现在写屏障不仅记录“老→新”指针,同时还要记录“黑→白”指针。一旦发现这样的指针,黑对象会被重新染色为灰对象,重新放回到双端队列中。当算法将该对象取出时,其包含的指针会被重新扫描,这样活跃的白对象就不会漏掉。
增量标记完成后,惰性清理就开始了。所有的对象已被处理,因此非死即活,堆上多少空间可以变为空闲已经成为定局。此时我们可以不急着释放那些空间,而将清理的过程延迟一下也并无大碍。因此无需一次清理所有的页,垃圾回收器会视需要逐一进行清理,直到所有的页都清理完毕。这时增量标记又蓄势待发了。
Google近期还新增了并行清理支持。由于脚本的执行线程不会再触及死对象,页的清理任务可以放在另一个单独的线程中进行并只需极少的同步工作。同样的支持工作也正在并行标记上开展着,但目前还处于早期试验阶段。
总结
垃圾回收真的很复杂。我在文章中已经略过了大量的细节,而文章仍然变得很长。我一个同事说他觉得研究垃圾回收器比寄存器分配还要可怕,我表示确实如此。也就是说,我宁可将这些繁琐的细节交给运行时来处理,也不想将其交给所有的应用开发者来做。尽管垃圾回收存在一些性能问题而且偶尔会出现灵异现象,它还是将我们从大量的细节中解放了出来,以便让我们集中精力于更重要的事情上。