Java垃圾回收算法
GC算法总体概述:JVM在进行GC时,并非每次都对上面三个内存区域一起回收,大部分时候回收的区域都是只新生代。因此GC按照回收的区域又分了两种类型,一种时普通的GC(Minor GC),一种是全局GC(Major GC或者Full GC)。
普通GC(Minor GC):只针对新生代区域的GC,
全局GC(Major GC或者Full GC):针对老年代的GC,偶尔伴随着对新生代的GC以及对永久代的GC。
垃圾回收算法主要分为4种:
- 复制算法
- 标记清除算法
- 标记整理算法
- 分代收集算法
复制算法:
年轻代使用的是Minor GC,这种GC算法采用的是复制算法(Copying)。
JVM把新生代分为了三个区域:1个Eden区和两个Survivor区(分别叫做from和to),默认比例是8:1:1,一般情况下,新创建的对象的都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被转移到Survivor区,对象在Survivor区没每熬过一次Minor GC,年龄就会增加一岁,当增加到一定程度(默认是15岁),就会被转移到老年代中,因为新生代中的对象基本上都是朝生夕死的(80%以上都会死亡),所以新生代的垃圾回收算法使用的是复制算法。
复制算法的流程:
- 当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden和From区域清空。
- 当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
- 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数-XX:MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
注意:from区和to区并不是固定的,哪一个Survivor 区为空,哪一个Survivor 区就为to区。
假设第一次Minor GC发生,只有如图所示绿色的两个区域的对象存活,则系统将会将该区域的对象复制到to区,将Eden和from区清空。当下一次Minor GC发生的时候,将Eden和to区存活的对象复制到from区,如此循环。(某些对象在经历一定次数的转移后会被转移到老年代)。
复制算法的优缺点:
优点:不会产生内存碎片;对象的完整度高。
缺点:浪费了空间(总会保存一个空闲的to区来放置Eden和from区存活的对象)。
标记清除算法:
算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
"标记-清除"算法的不足主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不高(两次扫描,耗时严重)
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大
对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
标记整理算法:
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
没有最好的算法,只有最合适的算法,因此,我们提出了分代收集算法:
当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活
周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用
复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算
法。