垃圾收集的算法有哪几种?说说利弊
有3种
标记-清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象, 在标记完成后, 统一 回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一 回收所有未被标记的对象。最基础的收集算法, 是因为后续的收集算法大多都是以标记-清除算法为基础, 对其缺点进行改进而得到的 。 它的主要缺点有两个: 第一个是执行效率不稳定, 如果Java堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过程的执行效率都随对象数量增长而降低; 第二个是内存空间的碎片化问题, 标记 、 清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-复制算法
常被简称为复制算法 。 为了解决标记-清除 算法面对大量可回收对象时执行效率低的 问题, 1969年Fenichel提出了一种称为“半 区复制”(Semispace Copying) 的垃圾收集 算法, 它将可用内存按容量划分为大小相 等的两块, 每次只使用其中的一块 。 当这 一块的内存用完了, 就将还存活着的对象 复制到另外一块上面, 然后再把已使用过 的内存空间一次清理掉 。 如果内存中多数 对象都是存活的, 这种算法将会产生大量 的内存间复制的开销, 但对于多数对象都 是可回收的情况, 算法需要复制的就是占 少数的存活对象, 而且每次都是针对整个 半区进行内存回收, 分配内存时也就不用 考虑有空间碎片的复杂情况, 只要移动堆 顶指针, 按顺序分配即可 。 这样实现简单, 运行高效, 不过其缺陷也显而易见, 这种 复制回收算法的代价是将可用内存缩小为 了原来的一半, 空间浪费未免太多了一点。
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代, IBM公 司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代 中的对象有98%熬不过第一轮收集 。 因此并不需要按照1 ∶ 1的比例来划分新生 代的内存空间。
在1989年, Andrew Appel针对具备“朝生夕灭”特点的对象, 提出了一种更优化 的半区复制分代策略, 现在称为“Appel式回收”。 HotSpot虚拟机的Serial 、 ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局 。Appel式 回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空 间, 每次分配内存只使用Eden和其中一块Survivor 。 发生垃圾搜集时, 将Eden 和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上, 然后直接 清理掉Eden和已用过的那块Survivor空间 。 HotSpot虚拟机默认Eden和Survivor 的大小比例是8 ∶ 1
标记-整理算法
标记-复制算法在对象存活率 较高时就要进行较多的复制操 作, 效率将会降低 。 更关键的 是, 如果不想浪费50%的空间, 就需要有额外的空间进行分配 担保, 以应对被使用的内存中 所有对象都100%存活的极端情 况, 所以在老年代一般不能直 接选用这种算法。
针对老年代对象的存亡特征,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact) 算法,其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存.
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收 算法, 而后者是移动式的 。 是否移动对象都存在弊端, 移动则内存回收时会 更复杂, 不移动则内存分配时会更复杂 。从垃圾收集的停顿时间来看, 不移 动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来 看, 移动对象会更划算。