前言
在介绍垃圾收集算法之前,我们需要先了解一个词“stop the world”,stop the world会在执行某一个垃圾收集算法的时候产生,JVM为了执行垃圾回收,会暂时java应用程序的执行,等垃圾回收完成后,再继续运行。如果你使用JMeter测试过java程序,你可能会发现在测试过程中,java程序有不规则的停顿现象,其实这就是“stop the world”,停顿的时候JVM是在做垃圾回收。所以尽可能减少stop the world的时间,就是我们优化JVM的主要目标。
1.标记-清除算法
(1)基本过程
标记-清除算法是一种最基础的算法,后面的收集算法都是根据这种算法的不足进行改进而得到的。分为标记和清除两个阶段。
- 标记:标记的过程就是遍历所有GC Roots,然后将GC Roots可达的对象标记为存活对象。
- 清除:遍历堆中所有对象,将没有标记的对象全部清除掉。
(2)缺点
- 效率问题:标记和清除效率太低(标记和清除都需要遍历全堆对象)
- 空间问题:标记清除之后会产生大量连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存从而不得不再次触发垃圾收集动作。
2.复制算法
为了解决标记-清除算法中的效率问题,出现了一种"复制"的收集算法。
(1)基本过程
它将内存按容量划分为大小相同的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。
(2)缺点
- 效率问题:当对象存活率较高时,复制次数过多,效率降低
- 空间问题:内存缩小了一半,需要额外的空间做分配担保(老年代)(当内存中所有对象100%存活的极端情况)
3.标记-整理算法
为了解决复制算法的缺点,有人提出了标记-整理算法
(1)标记:标记的过程就是遍历所有GC Roots,然后将GC Roots可达的对象标记为存活对象。
(2)整理:让所有存活的对象都向一端移动,然后直接清理掉端(“端”就是前面说的向一端移动)边界以外的内存。
4.分代收集算法
前言:在说分代收集算法之前,我们首先要知道一个概念——堆结构分代,这个概念对理解前面几种垃圾回收算法很重要。
- 堆结构分代的意义:堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。所以给堆内存分代就是为了提高垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。
- Java虚拟机根据对象对象存活的周期不同,一般将堆内存划分为新生代,老年代,永久代(对HotStop虚拟机而言)。
(1)新生代
- 新生成的对象优先存放在新生代中,现在的商业虚拟机都采用的复制算法来回收新生代。IBM公司的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1( 可以通过参数 –XX:SurvivorRatio 来设定 ),这样就能够充分利用内存空间减少浪费,那么将会留下10%的空间来存放回收后还存活的对象就足够了,如果不够时,就依赖老年代来进行分配担保。关于这部分具体的知识以及新生代中为什么要将空间分为两个Survivor区,请参考:
(2)老年代
- 部分对象会在From Survivor和To Survivor区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代,由此看出,老年代中对象的存活率很高的,所以采用复制算法来实现不太理想,就使用标记-整理算法或者标记-清理算法。
分代收集算法
所以分代收集算法就是根据各个年代的特点选择最适合的收集算法 。
- 所以在新生代中,对象的大量死亡,只有少数对象存活,复制算法最合适。
- 在老年代中,对象存活率高,没有额外的空间对它进行分配担保,所以标记—清理算法或者标记—整理算法值是最合适的。
- 给不同的代选择合适的收集算法,这也是堆结构分代的意义所在。