一、标记-清除算法
该算法分为标记和清除两个阶段。标记阶段基于可达性分析,具体原理可以参照我之前的博客文章。可达性分析阶段会标记出所有存活的对象(也可以标记死亡的对象,原理类似)。在标记完成后,将回收所有的死亡对象。具体图解如下:
从图中我们可以看出这个收集方法的缺点:就是会产生大量的不连续的内存碎片,有可能提前引发一次FullGC(),进而影响其他程序的工作。除此之外,如果JAVA堆中包含了大量的对象,那么标记的过程将十分费时,同时,清理的过程会随着对象的增多而耗费更多的时间。
二、复制算法
这个算法对内存做了一个分区,将其分为新生代和老年代。一般情况下,垃圾回收的过程主要发生在新生代,老年代的对象很少发生垃圾回收(可通过FullGC()回收)。
在新生代中,将新生代分为伊甸园区(Eden),幸存者From区和幸存者To区。这三个空间的大小在HotSpot中的默认值是8:1:1。
接下来说一下这个回收方法的原理:每次分配内存,只分配给Eden区和From区。在垃圾回收时,对于Eden区和From区中的对象进行标记,标记完成后,将所有存活的对象复制到To区。之后,清除From区和Eden区中的所有对象,再将From区和To区进行交换。也就是说两个幸存者区并不是一成不变的,而是动态确定的。具体过程如下:
从图中可以看出,相比于标记-清除算法,复制算法解决了内存碎片的问题,因为存活对象复制到To区的时候是顺序存储的,即一定可以保证连续性。但复制算法的最大缺点是,如果存活的对象数量过多,复制过程将非常耗时。故此方法不适合回收存活率高的内存空间。
还有一个问题,当To区的空间不足以容纳Eden区和From区中的所有对象时,将会有一部分对象晋升到老年代。
三、标记-整理算法
标记-整理算法是基于标记-清除算法演化而来的,其标记的步骤与标记-清除算法相同,但清除的过程并不是直接将死亡对象清除,而是将所有存活的对象移向一边,然后清除所有的死亡对象。
该方法相比于标记-清除算法,不会产生内存碎片,相比于复制算法,清理效率较高。但移动存活对象时,需要暂停用户进程才能进行,因此,用户体验极差。但如果不采用移动对象的手段的话,就只能维护一个空闲列表来记录来记录空闲的内存空间,开销会更大。
除了这三种以外,还有一种“和稀泥”的垃圾回收算法,就是平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,当内存碎片大到影响对象分配时,再进行一次标记-整理算法。CMS垃圾收集器就是基于这种算法。