序言
本篇文章会介绍 JVM 虚拟机中三种常用的垃圾收集算法,这是理解垃圾收集器的前提条件,不同的垃圾收集器所选择的实现算法不同。
垃圾收集算法
从如何判定对象消亡的角度出发,垃圾收集算法可分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,由于“引用计数式垃圾收集”在JVM虚拟机中并没有使用到,所以下文的介绍的所有算法都属于追踪式垃圾收集的范畴。
分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循“分代收集”的理论进行设计,它建立在两个分代假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储,这样可以专门针对不同的区域使用不同的垃圾收集算法。比如一个区域中的大多数对象都是朝生夕灭的,那么对于该区域之后去标记那些存活的对象而不是标记大量要死亡的对象,就能以较低的代价回收大量的空间,反之亦然。
在目前的商用 Java 虚拟机中,我们通常将 Java 堆划分为新生代和老年代两个区域。顾名思义,在新生代中,每次垃圾收集都会有大量的对象死去,而每次回收存活的少量对象,将会逐渐晋升到老年代中存放。
标记清除算法
标记清除是最早出现的垃圾收集算法,其思想为:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以标记存活的对象,统一回收掉所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
缺点如下:
- 执行效率不稳定。如果 Java 堆中包含大量的对象,而且大部分对象都是需要被回收的,此时就需要进行大量的标记和清除动作,导致执行效率低下。
- 内存碎片化:标记、清除后会产生大量的不连续的内存碎片,空间碎片太多会导致虚拟机因分配大对象而无法找到连续的内存空间不得不提前触发垃圾收集。
其示意图如下所示:
标记复制算法
标记复制算法是为了解决标记清除算法面对大量可回收对象导致执行效率低下所提出的一种算法。其思想为:它将可用内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存一次性清理掉。这样可以避免内存内存的问题。 缺点:
- 内存只使用了原来的一半,空间浪费过多。
其示意图如下所示:
这种算法常用于新生代区域。因为新生代多数对象都是朝生夕灭的,使用标记复制算法最合适。
由于空间浪费过多的问题。到1989年,提出了一种更优化的半区复制分代策略,称为“Appel 式回收”。Appel 式回收的具体做法就是把新生代分为一块较大的 Eden 空间和两块较小 Survivor 空间,每次分配只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 依然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。在 HotSpot 虚拟机默认的 Eden 和 Survivor 的大小比例式 8 : 1,即每次新生代可用的内存空间占整个新生代容量的 90%,只有一个 Survivor 空间是浪费的。
标记整理算法
标记复制算法在对象存活率较高时就要进行较多的复制操作,所以老年代不采用这个算法,而是采用标记整理算法。其思想为:标记过程依然与标记复制算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向内存空间的一段移动,然后直接清理掉边界以外的内存。其示意图如下所示:
移动对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全部暂停用户应用程序才能进行。
但是如果跟标记-清除算法那样不移动对象的话,那么堆内存碎片化的问题就只能使用更复杂的手段来解决。所以基于以上两点,不同的垃圾收集器存在着不同的实现方式。比如,HotSpot 虚拟机中最关注吞吐量的 Parallel Scavenge 收集器就是基于标记整理算法实现的,而关注延迟的 CMS 收集器则是基于标清除算法实现的。