《深入Java虚拟机》之垃圾收集算法

标记-清除算法

算法分两个阶段:

  • 标记:将所有不可达对象进行标记
  • 清除:清除所有标记的对象

当标记的对象满足以下条件,那么会加入F-Queue中等待第二次标记

  • 对象覆写finalize()且finalize()没有被虚拟机调用过

该算法有以下两个问题:

  • 效率:标记和清除的效率都不高
  • 空间:标记-清除后,内存里会产生大量不连续的空间,空间碎片太多会导致分配大对象时,再次触发GC

标记-清除算法示意图

复制算法

将内存空间划分为两个部分,每次都只往一个部分里写入对象。当这块(写入对象的那块)的内存空间用完,就将存活着的对象复制到另外一部分空间里面,然后把原来那部分清理掉。这样每次都只是对整个半区进行内存回收,内存分配时也不用考虑空间碎片等问题,只需移动堆顶指针,按顺序分配即可。

该算法的问题:

  • 空间:将内存缩小为原来的一半,代价太高

复制算法示意图

现代主流的虚拟机都采用这种收集算法来 回收新生代,但是在空间分配上进行了调整,根据研究结果——“新生代中的98%的对象是朝生夕死”的,所以不需要缩小一半;现在的主流做法是将内存分为一块较大的 Eden空间和两块较小的 Survivor空间,每次使用Eden和其中一块Survivor(Eden:Survivor1:Survivor2 = 8:1:1)。

当发生GC时,将Edent和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,然后清理掉Eden和刚刚用过的Survivor空间。然而不是所有场景下,存活的对象都不超过10%,所以当Survivor空间不足时,需要依赖其他内存空间(老年代)。

修改空间比例后的算法问题:

  • 效率问题:对象存活率较高时就要进行较多的复制,效率会变低
  • 极端情况:为应对对象100%存活的情况,需要有额外的空间进行担保

标记-整理算法

根据老年代的特点,有人提出了标记-整理算法(mark-compact),标记过程和前面一致,在清理过程时,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

标记-整理算法示意图

分代收集算法

根据对象存活周期将内存划分为几块。一般是分为 新生代老年代。说通俗点就是分而治之,新生代因为对象创建和回收都比较频繁,每次只有少量存活就可以采用复制算法;老年代因为对象存活率高、没有额外空间对他进行分配担保,就必须使用 标记-清除标记-整理 算法。