JVM GC 垃圾收集算法总结

jvm的垃圾收集算法总结起来有4种,严格来说应该是3种,下面一一详细介绍。由于垃圾收集算法涉及程序细节,而且各个平台的虚拟机操作内存的方法不同,因此这里不过多介绍算法的实现,注重几种算法的思想及发展过程。

1. 标记 - 清除算法

最基础的算法“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有要回收的对象,在标记完成后同一回收掉。之所以说它是最基础的收集算法,是因为后续的算法都是基于这种思路并对其不足进行改进得到的。
主要不足:

  1. 一个是效率问题,标记和清除两个效率都不高。
  2. 另一个是空间问题,标记清除后产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次GC。

标记-清除算法执行过程如图所示。

JVM GC 垃圾收集算法总结_第1张图片

从图中可以看出,有很多不连续的内存空间碎片。

2. 复制算法

为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用的内存空间分为大小相等的两块,每次只使用其中一块。当一块内存用完了,就将还存活的对象都复制到另一块上,然后再把自己使用的空间一次清理掉。这样使的每次都对整个半区进行内存回收,内存分配是不用考虑空间碎片问题,只要一动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
不足:
将内存缩小为原来的一半,每次只有一半的内存空间可用,代价高了一点。
复制算法的执行过程如下图:

JVM GC 垃圾收集算法总结_第2张图片

现在的商业虚拟机都采用这种算法回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以不需要按照 1: 1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中的一个Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8: 1 ,也就是每次新生代可用内存空间为整个新生代容量的 90%(80%+10%),只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法每次回收都只有不大于10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
分配担保的通俗讲法:比如我们去银行借款,如果我们信誉很好,在98%的情况下能按时偿还,银行可能也会默认我们能按时还款,只需要有个担保人能保证我们不能按时还款,可以从他的账户扣钱,那银行就认为没风险了。内存分配担保也一样,如果另一块 Survivor 空间没有足够空间来存放上次收集后还存活的对象,这些对象将直接通过分配担保机制进入老年代。

3. “标记-整理”算法

复制算法在对象存活率高的情况下要进行较多的复制操作,效率会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的内存空间分配担保,以应对被使用内存中所有对象都100%存活的极端情况,所以老年代一般不直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
回收过程如下图所示:

JVM GC 垃圾收集算法总结_第3张图片

4. 分代收集算法

当前商业虚拟机垃圾收集都采用分代收集(Generational Collection)算法,这种算法没有什么新的思想,只是根据对象存活周期不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样可以根据各个年代的特点采用最合适的算法。在新生代,每次垃圾收集时发现都有大批的对象死去,只有少量存活,那就选用复制算法,只需要付出少量的存活对象复制成本即可完成收集。而老年代中对象因为存活率高、没有额外的空间对它分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收♻️。

最后

贴上一张 JVM 内存分区示意图,加深一下各个分区的印象,有助理解分代收集的思路。

JVM GC 垃圾收集算法总结_第4张图片

你可能感兴趣的