从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(Reference Counting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
本节主要总结的垃圾回收算法均属于追踪式垃圾收集的范畴,常用GC回收算法有:标记清除,标记复制,标记整理和分代收集算法。
1.分代收集(Generational Collecting)算法
这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用“标记-复制”算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
2. 标记-清除(Mark-Sweep)算法
最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,在1960年由Lisp之父 John McCarthy所提出。
如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。
之所以说它是最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其 缺点进行改进而得到的。
“标记-清除”(Mark-Sweep)算法的主要缺点有两个:
- 第一个是执行效率不稳定。如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过 程的执行效率都随对象数量增长而降低;
- 第二个是内存空间的碎片化问题。标记、清除之后会产生大 量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-清除算法的执行过程如图所示。

3. 标记-复制(Mark-Copying)算法
标记-复制算法常被简称为复制算法。
为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题,1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存 活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。
标记-复制算法的执行过程如图所示。

4. 标记-整理/压缩(Mark-Sweep)算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。
更关键的是,如果 不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存 活的极端情况,所以在老年代一般不能直接选用这种算法。
针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可 回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内 存。
标记-整理算法结合了“标记-清除”和“复制”两个算法的优点。
算法运行分两阶段:
- 第一阶段从根节点开始标记所有被引用对象
- 第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。
“标记-整理”算法的示意图如图示。

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
标记-整理算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
















