标记-清除算法
标记-清除算法是最早出现也是最基础的垃圾回收算法。该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一清除被标记的对象。也可以反过来,标记所有存活的对象,然后清除所有未标记的对象。该算法有两个缺点:
- 执行效率不稳定,如果堆中存在大量的对象,并且这些对象大部分是需要被回收的,必须进行大量的标记、清除动作。
- 会产生空间碎片,由于该算法只是将需要回收的对象清除,存活的对象位置不变,清除后会产生大量的不连续空间,空间碎片太多可能会导致在需要给大对象分配空间时,找不到足够大的连续空间,从而提前触发垃圾回收。
网络异常,图片无法展示|
标记-复制算法
标记-复制算法就每次都将存活的对象复制到一块空的内存空间,最简单的做法是将内存空间分成两块同等大小的空间,每次只使用其中的一块空间,当内存不够时,触发GC,并将存活的对象全部复制到另一块空间,然后将旧的空间全部清除。如果内存中的大多数对象都是存活的,这种算法会产生大量的内存复制开销,但如果只有少数对象时存活的,那么只需要复制少量的对象,并且这种算法不会产生内存碎片。
上面的做法有一个弊端,会浪费50%的空间。现在的JVM大多数采用这种算法来回收新生代,由于新生代的对象都是“朝生夕死”的,根据IBM的研究,在新生代中有98%的对象是熬不过第一轮回收的,所以不需要按1:1的比例来划分新生代的内存空间。
目前主流的划分方式是将新生代划分成一个Eden区和两个Survivor区,每次内存分配只使用Eden区和其中一个Survivor区,发生GC时,将Eden区和已使用的那个Survivor区中存活的对象复制到另一个未使用的Survivor区。在Hotspot虚拟机中默认Eden和Survivor的比例是8:1,因为每次都使用Eden区和一个Survivor,所以只有10%的空间被浪费。由于不能保证每次存活的对象都低于10%,需要采用分配担保机制。
标记-整理算法
标记-整理算法在标记阶段,和标记-清除算法是一样的,而在整理阶段,不是直接将内存回收,而是将存活的对象全部移动到内存空间的一端,然后将边界后的内存直接清除,这样可以避免产生内存碎片。标记-整理算法和标记-清除算法的本质差异是标记-整理算法需要移动存活的对象,而标记-清除算法不需要。
由于标记-整理算法需要移动存活的对象,并需要更新所有引用该对象的地址,而且这种对象移动操作需要"Stop The Wold",所以标记-整理算法的回收效率是低于标记-清除算法的。
标记-清除算法虽然不需要考虑移动存活的对象,但由此导致的内存空间碎片,需要依赖更加复杂的内存分配器和内存访问器来解决,如通过“分区空闲分配链表”来解决内存分配问题。
从垃圾回收的停顿时间来看,不移动存活对象的停顿时间更短,标记-清除算法更占优势。而从程序的吞吐量来看,虽然移动存活对象需要停顿更多时间,但由于内存分配和访问相比垃圾回收更加频繁,这部分的耗时增加,总吞吐量仍然是下降的。在Hotspot中更关注吞吐量的Parallel Old收集器就是基于标记-整理算法,而更关注停顿时间的CMS则主要采用标记-清除算法,只有当内存空间碎片多到无法为大对象分配空间时,再采用标记-整理算法进行收集。