垃圾收集算法
1. 标记-清除算法
标记-清除算法(Mark-Sweep),是最早出现的最基础的垃圾收集算法,它分为标记和清除两个阶段。标记:标记出所有需要回收的对象。清除:在标记完成后,统一进行清除,也即回收掉垃圾对象的内存空间。作为最早出现的垃圾收集算法,后继的收集算法也是以标记清除算法为基础,对它的确定进行了改进。
标记-清除算法在老年代垃圾收集器使用的较多,对于老年代而言,多数对象都是长期存活的,因此,清除掉那些死亡的对象即可。但是标记清除算法还有两个明显的缺点:
1. 效率问题:当每一次都要标记大量对象时,且其中大部分对象都是需要被回收的时候,就必须进行大量标记清除的动作。标记和清除都会随着对象数量的增加而效率降低。
2.空间碎片问题:标记、清除之后会产生大量不连续的内存空间-我们称之为空间碎片,这些空间碎片存在于对象和对象之间,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2. 复制算法
复制算法,又称为标记-复制算法,是为了解决标记-清除算法在处理大量可回收对象时带来的问题而提出的算法。该算法将可用内存空间分为大小相同的两块区域,每次使用其中的一块区域。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
从图中可以看出,标记-复制算法一次使用一块空间区域,进行垃圾收集的时候,将存活对象移入到另一块区域中,这样就解决了空间碎片的问题;并且由于它只需要每次标记存活的对象然后进行复制,最后一次性清理整块区域,因此其运行的效率也比标记清除算法要高效。
不过,这样带来了很明显的缺陷:可用空间分成相同大小的两份,每次只使用其中一份,带来了巨大的空间浪费。
标记-复制算法一般多用于年轻代垃圾收集器中,我们都知道hotspot虚拟机将年轻代分为了Eden区和Survivor区域(除开最新的ZGC),在对这块区域的对象进行垃圾回收的时候,就是采用了标记-复制算法,将所有新生的对象都放在Eden区域中,并且新生代对象中大部分对象都是朝生夕死,因此存活的对象是非常少量的,在进行MinorGC的时候,仅仅需要将其中少数存活的对象移入到Survivor区域中。将较小的Survivor划分为两块区域,即使每次只使用其中一部分,也不会有多大的空间浪费,并且也不存在空间碎片的问题,Eden区和Survivor区大小不一样的划分就是解决标记-复制算法空间带来问题而提出的模型。
虽然对于大多数场景对象都是朝生夕死,但是总会有特殊情况导致大部分对象都会存活,而Survivor区域无法容纳,就需要依靠其它区域来解决(也即老年代),这也就是前面的老年代空间分配担保机制存在的原因。
3. 标记-整理算法
标记-整理算法,是根据老年代的特性特别设计的一种算法。该算法先对空间内存活的对象进行标记后,然后让所有的存活对象都像内存空间的一端移动,然后直接清理掉末尾之外的内存。
对于老年代而言,如果采用标记复制算法,因为其对象的存活率较高,需要进行复制的对象较多,效率就会降低,并且会浪费大量的内存空间。而如果使用标记-清除算法,不用考虑移动时带来的效率问题,但是其存在的空间碎片问题则需要通过内存分配器来将对象拆开了分配到不同的空间中,这样就加大了整个jvm的复杂度。而采用标记整理算法同样有因对象移动而带来的效率问题,并且在垃圾回收时用户线程是必须暂停的,移动对象的过程则延长了这个停顿的时间,这就是“Stop The World”的现象。
4. 选择合适的算法
对于标记-清除算法和标记-整理算法而言,各有各的优缺点,标记-清除算法由于少了移动对象的过程,它的停顿时间比标记-整理算法要少很多,因此在关注低延迟的场景下,选择标记-清除算法,但是由于碎片空间的问题,导致了其进行GC的次数增多,从而导致整体吞吐量降低,因此在关注吞吐量的场景下,则选择标记-整理算法要好。
吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 运行垃圾收集的时间)
如何理解?比如,使用标记-整理算法的时候,运行用户代码时间为10s,执行gc时间为200ms,这样用户的延迟时间为200ms。而如果使用标记清除算法,由于有内存空间碎片的原因,导致了其每运行5s时间,就要执行一次gc,gc时间为130ms,而用户每一次延迟只有130ms,但是整体而言总的停顿时间变多了,也就是吞吐量变低了。
当然,也可以将两种算法都进行使用,比如,在多数时间下,采用标记-清除算法,短期内忍耐空间碎片的问题,直到空间碎片问题最大程度地影响到对象内存分配时,再采用标记-整理算法收集一次,对内存空间进行一次整理,这种用法已经在CMS垃圾收集器上使用(CMS垃圾收集器在后面会有讲到)。
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。