垃圾收集算法有标记-清除算法、复制算法、标记真理算法、分代收集算法四种,下面我们详细介绍。
标记-清除算法:
最基础的收集算法是标记-清除算法,如同它的名字一样,分为标记和清除两个阶段。第一步标记出所要回收的对象,在标记完成后统一回收所有被标记的对象。如何标记已经在上面说过了,之所以说它是最基本的垃圾收集算法,原因在于其他的算法也是基于这种思路并对其不足做以改进得到的。
主要问题有两个:
第一个是效率问题,标记和清除的效率都不高。
第二个是空间分配问题,标记清除后会产生大量的不连续的内存空间,空间碎片太多可能会导致以后程序在运行过程中需要给较大对象分配空间时,无法找到足够的内存空间,而不得不提前进行一次垃圾收集动作。如图所示,会产生大量的垃圾碎片,导致空间的利用率不高。
复制算法
为了解决效率问题,一种称为复制的收集算法出现了,它将可用内存分为大小相等的两块,每次只使用其中的一块,当这一块内存区域用完了,就将还存活的对象复制到另一块内存中,然后再把已使用的空间一次性清理掉,这样每次都是对半个区域进行回收,内存分配时也就不用考虑碎片等问题了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
只是这种做法将原来的内存缩小为一半,代价太高了。
现在的商用虚拟机都采用这种方法来回收新生代,IBM专门研究表明,新生代中的对象98%都是"朝生夕死"的,所以并不需要按照1:1划分内存区域,而是将内存分为一块较大的区域给Eden和两块较小的区域给Survivor, 当回收时,将Eden和Survivor区中还存活的对象一次性复制到另一块Survivor区,然后将Eden和Survivor区进行一次性清理。Hotspot区默认的Eden和Survivor的比例为8:1,也就是说新生代的可用内存为90%,只有10%的内存会被划分为保留内存。当然,大多数情况下是98%,但我们不能保证每次回收的存活对象都小于10%,当Survivor区不够用时,需要依赖其他区域进行分配担保。如果另外一块Survivor区已经不够用了,对象可通过内存担保机制直接进入到老年代。
标记整理算法
复制算法在存活对象比例比较高的情况下要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的区域,则需要额外的空间进行分配担保,以应对内存中100%对象都存活的极端情况,所以老年代一般不选用这种算法。
根据老年代的特点,有人提出了另一种标记-整理算法,标记过程与标记-清除算法一致,但后续步骤不是对可回收对象直接进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界外的对象。示意图如下:
分代收集算法
当前商用的垃圾收集器都采用的是分代垃圾回收,这种算法没有什么新的思想,只是根据对象的存活周期将内存分为几块,一般是将java堆分为新生代和老年代,这样就可以根据各个代的对象特点选用最适当的回收算法。在新生代,每次垃圾回收都有大量的对象死去,只有少量存活,这样就适合采用复制算法。只需要付出少量的对象复制成本就可以完成垃圾回收,而老年代因为存活率高,没有其他内存进行分配担保,就必须使用标记-清理或者标记-整理进行回收。
1. 分代分为年轻代和老年代,年轻代里头又分为 Eden区和Survivor区,通常默认的比例为8:1:1, 每次只保留10%的空间用作预留区域,然后将90%的空间可以用作新生对象。
2. 每一次垃圾回收之后,存活的对象年龄对应+1,当经历15次还依然存活的对象,我们让它直接进入到老年代;
3. 另外一种进入到老年代的方式是内存担保机制,也就是当新生代的空间不够的时候,对象直接进入到老年代;
4. 新生代的垃圾回收叫Minor GC,老年代的叫Full GC。