整理一下垃圾收集算法,涉及到的所有算法均属于追踪式垃圾收集的范围。
分代收集理论
1.强分代假说
熬过越多次垃圾收集过程的对象就越难以消亡。
2.弱分代假说
绝大多数对象都是朝生夕灭的。
如果一个区域中大多数对象都是朝生夕灭, 难以熬过垃圾收集过程的话, 那么把它们集中放在一起,每次回收时只保留少量存活的对象, 就能以较低代价回收到大量的空间 如果剩下的都是难以消亡的对象, 那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域, 这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
3.跨代引用假说
这其实是可根据前两条假说逻辑推理得出的隐含推论: 存在互相引用关系的两个对象, 是应该倾向于同时生存或者同时消亡的。 举个例子, 如果某个新生代对象存在跨代引用, 由于老年代对象难以消亡, 该引用会使得新生代对象在收集时同样得以存活, 进而在年龄增长后晋升到老年代中, 这时跨代引用也随即被消除了。
这种关系会增加一些运时的开销。
标记—清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象, 在标记完成后, 统一回收掉所有被标记的对象, 也可以反过来, 标记存活的对象, 统一回收所有未被标记的对象。 标记过程就是对象是否属于垃圾的判定过程
他的主要缺点:
- 执行效率不稳定
如果Java堆中包含大量对象, 而且其中大部分是需要被回收的, 这时必须进行大量标记和清除的动作, 导致标记和清除两个过程的执行效率都随对象数量增长而降低;
- 内存空间的碎片化
标记、 清除之后会产生大量不连续的内存碎片, 空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记—复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,提出了一种称为“半区复制” 的垃圾收集算法, 它将可用内存按容量划分为大小相等的两块, 每次只使用其中的一块。 当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面, 然后再把已使用过的内存空间一次清理掉。每次都是针对整个半区进行内存回收, 分配内存时也就不用考虑有空间碎片的复杂情况, 只要移动堆顶指针, 按顺序分配即可。
优点:
- 实现简单,运行高效
缺点:
- 将可用内存缩小为了原来的一半, 空间浪费大
针对他的空间浪费大的缺点,有的公司提出了更好的方法,因为大多数新生代对象都不会熬过第一次 GC(垃圾收集)。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。
标记—整理算法
针对老年代对象的存亡特征, 提出了另外一种有针对性的“标记-整理”(Mark-Compact) 算法, 其中的标记过程仍然与“标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向内存空间一端移动, 然后直接清理掉边界以外的内存,标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
通过上述算法我们可以知道,JVM中的新生代每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记—清除 或者 标记—整理 算法回收。