一、引文
在“为什么会有分代收集器算法“一篇中,我们了解了Java堆为什么划分成新生代和老年代。这时面试官就会说:小伙子可以啊,那你介绍一下垃圾收集算法吧。
好,那就让我们进入今天的主题吧,垃圾收集算法。
垃圾收集算法主要为三大类:标记-清除、标记-复制、标记-整理。每个算法都有各自的特性以及优缺点。
二、标记-清除算法
介绍:
“标记-清除”(Mark-Sweep)是最早出现也是最基础的垃圾收集算法, 后续很多算法在其基础上做了衍生。该算法被分为两个阶段:“标记”和“清除”,这个大家看其名字也可以知道。
过程:
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。怎么标记其实就是如何判断一个对象是垃圾的方法。
存在问题:
- 执行效率不稳定, 如果堆中对象都为垃圾,那么这个标记和清除的时间将会变长。也就是说该耗时受堆内可回收对象数量的影响。
- 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致无法为较大对象足够的连续内存,从而不得不再次触发垃圾收集动作。
三、 标记-复制算法
介绍:
该算法简称“复制算法“,由于”标记-清除算法”遇到大量可回收对象时执行效率低的问题,后续Fenichel提出了“半区复制”(Semispace Copying) 的垃圾收集算法。
过程:
该算法将内存一分为二,每次只用半块,当其中一块内存用掉就把存活对象复制到另外一块并且将已使用过的内存空间一次性清除掉。
存在问题:
- 如果内存中都为存活对象(可能性很低,之前已经介绍过了对象的存活特性,大部分对象基本在第一轮收集之后就会被回收),那么就会产生大量的内存间复制的开销,但实际上只会有少数存活并且只需要移动堆顶指针,按顺序分配即可,也不用考虑空间碎片这种复杂的情况。其主要缺点就是可用内存缩小为了原来的一半,空间浪费太大。
算法优点:
- 实现简单,运行高效。
优化版:
1989年Andrew Appel提出了一种半区复制分代策略“Appel式回收”,专门处理“朝生夕灭”特点的对象。HotSpot虚拟机的Serial、 ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。
过程:
将新生代分为一块较大的Eden空间和两块较小的Survivor空间(也就是s0和s1), 每次分配内存只使用Eden和其中一块Survivor。进行垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一个Survivor上, 然后清理掉Eden和已用过的Survivor空间。 HotSpot虚拟机默认Eden和Survivor的大小比例是8∶ 1:1,即10%的新生代空间会把浪费。
那这个设计有什么问题呢?
如果有存活对象大于10%怎么办呢?针对这种极少发生的情况,Appel式回收设计了“逃生门”。当Survivor空间不足以容纳一次Minor GC之后存活对象时,就依赖其他内存区域进行分配担保(Handle Promotion),主要使用老年代。当前Survivor内存不够时,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代。 进行这样的担保,老年代本身必须还要有容纳这些对象的剩余空间,但实际有多少对象能存活下来,在内存回收之前是无法确定的,所以就会采用历史晋升到老年代对象容量的平均大小作为经验值与老年代的剩余空间进行比较,最终决定是否进行Full GC来让老年代腾出更多空间。
四、 标记-整理算法
介绍:
当上述存活对象较多时,标记复制算法的效率将会降低。特别是老年代的对象一般难以回收,针对老年代的存活特点,Edward Lueders “标记-整理”(Mark-Compact)算法。
过程:
标记-整理其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。“标记-整理”和“标记-清除”的本质区别是否采用移动式的回收。
存在问题:
在老年代每次回收都有大量对象存活,移动存活对象并更新所有引用这些对象的该操作成本极高,并且此处还要进行 “Stop The World”。但使用“标记-清除算法”堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。因此是否移动对象都存在弊端,移动则内存回收时会更复杂,不移动则内存分配时会更复杂。从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
其他:
当然还有一种混合式的解决方案可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。