找到了活对象后,接下来要做的就是将活对象进行复制,将其复制到堆2区。当然,复制到堆2区的对象间的内存地址是连续的,如果要分配新的内存空间的话,直接从堆空闲的一段分配即可。这样在分配内存空间时的效率是比较高的。对象复制后,要修改来自“非堆区”的引用地址。如下所示。
复制完毕后,我们直接将堆2区的中的所有内存空间进行回收即可,下方就是复制回收后的最终结果。下方的堆1区清空后,可以接收复制过来的对象了。当对堆2区进行垃圾回收时,会把堆2区的活对象拷贝到堆1区上。
从该实例中我们可以看出当内存垃圾特别多的时候“复制式”垃圾回收的效率还是比较高的,因为复制的对象比较少,清除时直接将旧的堆空间进行清理即可。但是,当垃圾比较少的时候,这种方式会复制大量的活对象,效率还是比较低的。这种方式也会将堆的存储空间进行分半。也就是说,总有一半是空闲的,堆空间的利用率不高。
3、标记-压缩回收算法
从上述“复制式”垃圾回收过程中,我们知道,垃圾多时其效率比较高,而垃圾少时,其工作方式效率是比较低的。那么,接下来,我们来介绍另一种标记-压缩回收算法,这种算法在垃圾少时的工作效率比较高,而垃圾多的情况下,工作效率反而不高,这就与“复制式”形成了互补。下方我们将会对标记-压缩回收算法进行介绍。
标记-压缩的第一部就是标记,需要将堆区中的“活对象”进行标记。上面的内容我们已经聊了什么是“活对象”,在此就不做过多赘述了。由“活对象”的特征我们可以看出,下方的活对象是内存区域1和3,所以我们将其进行标记。
标记完成后,我们就开始进行压缩了,将活对象压缩到“堆区”的一段,然后将剩余的部分进行清除。下方就是将1和3这两个活对象进行了压缩。压缩后,将下方的空间进行Clean。也就是说Clean的部分,就可以分配新的对象了。
下方截图是标记-压缩清理后的状态。标记-压缩式垃圾回收可充分利用堆区的空间,当垃圾比较少时,这种处理方式效率还是比较高的,如果垃圾太多碎片化严重时,移动的“活对象”较多,效率比较低。这种方式可以与“复制式”结合使用,根据当前堆区的垃圾状态来选择哪种回收方式。正好与“复制式”形成优势互补。将“复制式”、“标记-压缩式”的回收方式进行整合的算法,就是“分代式”垃圾回收机制,下方会详细介绍到。
4、分代式垃圾回收
“分代”即根据对象易产生垃圾的状态或者对象的大小将其分为不同的代,可分为“年轻代”、“年老代”和“永久代”。“永久代”不在堆中,再次先不做讨论。根据分代垃圾回收的特点,画了下方的简图。
在堆中,主要把区域分为“年轻代”、“年老代”。位于“年轻代”的对象内存创建的时间不长,更新比较快,易产生“内存垃圾”,所以“年轻代”的垃圾回收使用“复制式”回收方式效率比较高。“年轻代”又可分为两个区,一个是Eden Space(伊甸园)和Survivor Sprace(幸存者区)。Eden Space去主要存放那些初次被创建的对象,而Survivor Sprace存放的是从Eden Space幸存下来的“活对象”。在Survivor Sprace(幸存者区)中又分为form和to两块,用于相互复制对象来进行垃圾清理。
而“年老代”中存放的是一些“大对象”以及从Survivor Sprace中存活下来的“对象”,一般到“年老代”的对象比较稳定,产生垃圾较少,针对这种情况,使用“标记-压缩”式回收效率比较高。“分代垃圾回收”主要是分而治之,根据不同对象的特点将其分类,根据分类的特点来具体选择合适的垃圾回收方案。
三、分代式垃圾回收的具体工作原理
当然在JVM具体的垃圾回收时,根据线程分可分为使用单个线程回收的“串行垃圾回收”,使用多个线程回收的“并行垃圾回收”。根据程序的挂起状态,又可分为“独占式回收”和“并发式回收”。当然之前也多次聊过“并行”与“并发”绝对不是一个概念,切不可将其混淆。本篇博客就不对上述这些方式进行详述了,感兴趣的,请自行Google。
下面我们来看一下“分代式垃圾回收”的具体工作原理的完整步骤,来直观的感受一下“分代式”的垃圾回收的执行方式。
1、垃圾回收前
下图是等待“分代垃圾回收”的简图,从下图中,我们可以看出在堆中有些已分配的对象内存并没有被栈上引用,这些就是要被回收的对象。我们可以看出,下方的堆,整体上分为“年轻代”和“年老代”,而年轻代,有可细分为Eden Space, From以及To三个区域。关于每个区域的作用,在上面介绍“分代垃圾回收”时,我们已经介绍过了,所以在此部分我们不做详细介绍了。
2、分代垃圾回收