引言:
前面的文章提到,在8版本以后,Java内存区域:Heap包括了PSYoungGen、ParOldGen,以及堆外内存MetaSpace。JVM 在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是新生代。由于新生代和老年代的内存空间大小不同以及对象存活率不同,所以针对不同区域JVM采用了不同的GC,不同的GC是通过不同的算法实现的。在Jdk8中,按照回收区域的不同,把GC分为工作在新生代的普通GC(minor GC)和工作在堆全局空间的全局GC(Full GC)。
由于新生代和老年代占比空间为1:2,且采用了不同的算法,所以minor GC 的速度要比Full GC快很多。
一、复制算法
HotSpot 把新生代分为三个部分:Eden区和两个Survivor区(From区和To区),默认比例8:1:1。对象创建时会被放在Eden区,当Eden区触发GC(minor GC),GC会对Eden和Survivor区进行垃圾回收,幸存下来的独享会被 “复制” 到Survivor1区(To区),然后清空Eden和From区,最后将To和From交换,让刚才被清空的From作新的To区,让刚才保存对象的To区作新的From区,以保证下一次GC可以扫描到这些对象。这个过程中涉及到了一个 “复制” 的操作,就是 “复制算法” 的实现。顺带一提:当一个对象在多次GC后依然无法被回收,在From区和To区来回复制,每复制一次“年龄”加1,一旦“年龄”达到MaxTenuringThreshold的值(默认为15)就会被移动到老年代。
为了方便描述,这里将minor GC的扫描区域(Eden、From)简称为From区,因为这两块区域的共同特点就是在复制幸存对象到To区后会被清空,唯一的区别就是Eden用来保存第一次new出来的对象,而From区保存的则是经过若干次GC后任然幸存的对象。
整个流程如下图所示:
红色为幸存对象,黄色为被GC回收的对象,绿色表示闲置空间。当触发GC后,Eden区和From区的幸存对象会被复制到To区,然后清空Eden区和From区,最后将From区和To区对调,以保证下一次GC的正常工作流程。这些内容在前面介绍堆参数时也有提及,这里不再赘述。需要补充的是 “复制算法” 的优缺点:
优点:1、由于“复制算法”采用了复制—清空的方法,所以不会导致内存空间的碎片化。
缺点:1、由于复制算法需要另外的空间来 “周转” 这些幸存的对象,所以内存消耗比较大。
2、如果存在“极端情况”,比如大量的对象循环引用而导致无法回收的幸存对象占比很大,假设为80%,那么就需要将这些数量庞大的对象都复制一遍,并将所有的引用地址重置一遍,这回耗费比较多的时间。所以复制算法的最佳工作环境就是这一块的对象存活率比较低,所以在新生代中采用了这一算法。
二、标记清除
这是GC在老年代中的工作方式,标记清除算法分为两个阶段:标记阶段和清除阶段。
这种算法虽然不需要多余的内存空间 “周转” 对象,但是会导致内存碎片化。于是便引出了标记压缩算法
三、标记压缩
标记压缩其实就是在标记清除后加了一个 “压缩” 操作,将分散的数据压缩到一块连续的内存空间。就是慢,但慢工出细活。
四、更加优秀的选择
针对老年的GC,标记清除和标记压缩都不完美,最好的方式是组合使用,在多次使用标记清除后进行一次压缩。总的来说四种方式没有孰优孰劣,只有谁更合适。总结一下就是:
执行效率(算法的时间复杂度):复制算法>标记清除>标记压缩
内存整齐度:复制算法=标记压缩>标记清除
内存利用率:标记清除=标记压缩>复制算法
在Java9默认采用了G1垃圾回收器,采用了时间复杂度和空间利用率都非常出色的算法。