不过,它无法立即检测到垃圾。从动画中可以看到,在一小段时间内没有出现红色闪烁,之后又突然出现了很多红色闪烁,说明它正在标记存活的对象。在完成标记之后,所有的垃圾被清除,释放出内存。我们还能从动画中看到,有些区域一下子全部转成黑色,而不是像引用计数算法那样慢慢地随时间扩散开来。
标记清除算法有更高的一致性要求,而且难以将其集成到已有的系统中。在标记阶段,回收器要求遍历所有的存活对象,包括封装在对象里的数据。如果某个对象不支持回收器的遍历访问,那么使用这种回收器就会存在风险。标记清除算法的另一个缺点是,在清除阶段,它会在整个内存范围内进行清除。如果系统的垃圾不多,那么问题不大,但是现代的函数式编程模型总是能产生大量的垃圾。
标记整理回收器(Mark Compact Collector)
你可能会注意到,在之前的动画里,对象不会发生移动。一旦对象分配到了内存,它就会待在原地不动,即使内存出现了很多碎片。后面的两个算法将会改变这种状况,它们使用不同的方式来实现回收。
标记整理算法清理内存的方式不是通过清除,而是将对象移动到空余的内存空间。对象总是以不变的次序存留在内存里,先分配到内存的对象总处于较低的内存段,不过因为经过移动,对象间的内存间隙被消除。
也就是说,新创建的对象总是处于内存的高段。这种内存分配器被称为“bump”,类似于栈的分配,只是没有栈那样的大小限制。有些使用bump分配器的系统甚至不用调用栈来存储数据,它们直接在堆里分配调用帧,并把它们看成对象。
从理论上看,这种回收算法的另一个优势在于,程序具有了更好的内存访问模型,这种模型对现代硬件的内存缓存更加友好。不过,相比其他算法,我们很难直观地感受到这种优势,因为引用计数和标记清除算法里所使用的内存分配器虽然很复杂,但它们很健壮,很高效。
标记整理是一种复杂的算法,需要多次遍历所有分配到内存的对象。从动画里我们可以看到,在红色闪烁之后,在计算对象的目标地址时发生了很多读写操作,然后对象被移动到目标地址上,对象的引用也被指向新的地址。这种复杂性所带来的主要好处就是极低的内存开销。Oracle的Hotspot虚拟机使用了多种垃圾回收算法,其中老年代空间使用的是标记整理算法。
复制回收器(Copying Collector)
我要演示的最后一种算法是大部分高性能垃圾回收系统的基础。它有点类似标记整理算法,不过相比之下要简单很多。它把内存分为两个部分,然后在这两个内存空间之间移动对象。在实际应用当中,一般会有多个内存空间,每个空间分配给不同年代的对象。先是在其中一个内存空间创建新对象,如果它们存活下来,就把它们复制到另一个内存空间。最后,如果这些对象的寿命足够长,它们会被复制到老年代空间。如果一个垃圾回收器被贴上分代或者朝生夕灭的标签,那它极有可能是多空间的复制回收器。
除了简单性和灵活性,这种算法的最大优势在于,它只处理存活的对象。这种算法并不存在标记阶段,存活的对象直接被复制到新的地址上,对象引用也随之指向新的地址。
从动画中我们可以看到,有些对象集合几乎是整块地被复制到另一个内存空间里,这是比较糟糕的情况,这也就是为什么我们需要对垃圾回收器进行调优。如果我们能够通过调整内存大小和控制内存分配,确保大部分对象在垃圾回收开始之前死亡,那么,我们就得到了函数式编程和高性能的一个完美组合。