目录
一、分代收集理论
二、标记-清除算法
三、标记-复制算法
四、标记-整理算法
一、分代收集理论
分代收集理论建立在两个分代假说之上:
1、弱分代假说:绝大多数对象都是朝生夕灭的。
2、强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款垃圾收集器一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
设计者一般至少会把Java堆划分为新生代和老年代两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
二、标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
1、执行效率不稳定。如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量的增长而降低。
2、内存空间的碎片化问题。标记、清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
三、标记-复制算法
半区复制,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动栈顶指针,按顺序分配即可。
优点在于实现简单,运行高效,其缺点是将可用内存缩小为原来的一半。
不过新生代中的对象(大概是98%)绝大部分都熬不过第一轮收集,因此并不需要按照1:1的比例来划分新生代的内存空间。
Appel式回收: 把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1。也即每次新生代中可用内存空间为整个新生代容量的90%。任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多数就是老年代)进行分配担保。
四、标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%空间,就需要有额外的空间进行分配担保,以应对算法中的极端情况。一般老年代不采取这种算法。
标记-整理算法中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接堆可回收对象进行清理,而是让所有存活的对象都向内存空间一段移动,然后直接清理掉边界以外的内存,核心是移动。
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动的操作必须全程暂停用户应用程序才能进行,被最初的虚拟机设计者描述为“Stop The World"。不过相较于标记-清除算法,可以较好规避空间碎片化问题,此问题只能依赖更为复杂的内存分配器和内存访问器来解决。
有一种方案,做法是让虚拟机大多数时候采取标记-清除算法,直到内存空间碎片化程度大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。例如基于标记-清除算法的CMS收集器就是采纳的此种方案。