垃圾回收算法

  • 标记 - 清除算法
  • 复制算法
  • 标记 - 整理算法
  • 分代收集算法

1 标记 - 清除算法

实现原理

  • 标记出所有需要回收的对象。
  • 统一回收所有被标记的对象。

特点

  • 标记和清除效率不高。
  • 产生大量不连续的内存碎片。
  • 标记清除算法是最基础的收集算法,后面的几种算法都是基于这种思路并对其不足进行改进而得到的。 算法分为“标记”和“清除”两个阶段。
  • 标记 : 首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记的判定就是通过可达性分析算法分析出可回收对象。
  • 清除 : 标记完成后会回收对象。但是这其中存在着两个不足问题 : 第一是标记和清除这两个过程效率不高。其次是清除之后会产生大量不连续的内存碎片。
  • 清除 : 标记完成后会回收对象。
  • 但是这其中存在着两个不足问题 : 第一是标记和清除这两个过程效率不高。其次是清除之后会产生大量不连续的内存碎片。
  • 空间碎片太多可能会导致以后在程序运行过程中需要分配比较大对象时,无法找到足够连续内存。从而提前触发依次GC。
垃圾回收算法_第1张图片
160f7b667fa1cd3b.png
  • 相当于生活中标记不需要的物品,然后找几个人把这些东西丢了。 但是由于房间的空间是有限的,如果一直没有整理房间,如果有大件家具搬进来时,没有足够的空间可能要再标记清除一下不需要的物品,如果还是没有办法放下这件大家具,抛出OOM异常。丢(Out)到(Of)门外(Memory)。

2 复制算法

实现原理

  • 将内存划分为大小相等的两块。
  • 一块内存用完之后复制存活对象至另一块。
  • 清理另一块内存。

特点

  • 实现简单,运行高效。
  • 浪费一半空间,代价大。
  • 为了解决效率问题,复制收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
  • 当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的那一块内存空间全部清理。
  • 这样便不用考虑内存碎片问题,只要移动堆顶指针,重新按顺序分配内存即可,实现简单,运行高效。
  • 只是这种算法的代价是将活动使用的内存缩小为原来的一半。
垃圾回收算法_第2张图片
160f7b667fa1cd3b.png
  • 复制算法当存活对象越少(或者垃圾很容易产生)时,它的效率越高。
    也就是你只要将一些少量需要用到的对象丢到另一片干净的内存中,就可以轻松的大扫除。

换句话说,如果你需要保留的对象很多,那么便需要又费力又频繁的搬动到另一块内存中。

  • 复制收集算法在对象存活率较高时就要进行较多的操作,效率就会变得很低。并且只能使用50%的空间。

3 标记 - 整理算法

实现原理

  • 标记过程与 ”标记-清除“ 算法一样。
  • 存活对象往一端进行移动。
  • 清理其余内存。

特点

  • 避免 ”标记-清除” 算法导致的内存碎片。
  • 避免复制算法的空间浪费。
  • 标记 : 因为存活的对象比较多,我们将这些对象标记起来。然后接下来整理到一块。
  • 整理 : 让所有标记过的对象都向一端移动,然后直接清理掉端边界以外的内存。(按内存地址一次排列)
垃圾回收算法_第3张图片
160f7b66a84fa47a.png

4 分代收集算法

特点

  • 结合多种收集算法的优势。
  • 新生代对象存活率低 => “复制” 算法(注意这里每一次的复制比例都是可以调整的,如一次仅复制 30% 的存活对象)。
  • 老年代对象存活率高 => “标记-整理” 算法。

当前很多商业虚拟机都采用分代收集(Generational Collection)算法作为垃圾收集器。

  • 这种算法只是根据对象存活周期的不同将内存划分为几块,进而采用不同的回收算法的策略。
  • 一般是把Java堆分成新生代和老年代。
  • 新生代 : 对象朝生暮死,存活的对象少,可回收对象多。
    选用复制算法,只要付出少量存活对象的复制成本就可以完成收集。
  • 老年代 : 对象存活率高,回收的对象很少。
    选用标记 - 清理算法,或者标记 - 整理算法来进行回收。
  • 对于新生代采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用复制算法效率最高。
  • 但实际中并不是按照上面算法中说的1:1的比例来划分新生代的空间的,而是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1(比例可以调整)。
  • 每次使用Eden和其中一块Survivor,当回收时,将Eden和刚才用过的Survivor空间中还存活的对象一次性的复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。(后面会继续研究,如果这次复制的对象大小超过Survivor空间,将会有分配担保机制。)
  • 由于老年代的特点是每次回收都只回收少量对象,使用的是标记 - 清理算法,或者标记 - 整理算法来进行回收。它与新生代的比例为 2(老年代) : 1(新生代)。当然这个比例也是可以调整的。
垃圾回收算法_第4张图片
160f7b66a6600310.png

分代收集算法工作流程

  • 1 . 分配了一个又一个对象 : 放到Eden区。
  • 2 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
  • 3 . 又分配了一个又一个对象 : 放到Eden区。
  • 4 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区。
  • 5 . 又分配了一个又一个对象 : 放到Eden区。
  • 6 . 不好,Eden区满了,只能GC(新生代GC:Minor GC)了 : 把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
    为什么不是一块Survivor空间而是两块?
  • 这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次(仅供参考)GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到Survivor A空间,但是第二次GC的时候,Survivor A空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。 所以,这里就需要两块Survivor空间来回倒腾。

为什么Eden空间这么大而Survivor空间要分的少一点?

  • 新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。
    我看8:1:1就挺好的,当然这个比例是可以调整的,包括上面的新生代和老年代的1:2的比例也是可以调整的。
    新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

参考

Java垃圾回收(一)—— 回收机制

你可能感兴趣的:(垃圾回收算法)