垃圾回收算法&垃圾收集器

垃圾回收算法

标记清除 (Mark-Sweep)

算法分为两个阶段:标记 和 清除,首先标记出所有要回收的对象,在标记完成之后统一回收所有标记的对象。它有两个不足:

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

Mark-Sweep

标记整理 (Mark-Compact)

标记整理算法过程与标记清除算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

Mark-Compact

复制 (Copying)

算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就将还存储着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

效率高,实现简单,但是代价是将内存缩小为原来的一半。现在的商业虚拟机都是使用这种方法来回收新生代(Eden Survivor)。

Copying

垃圾收集器

CMS收集器(Concurrent Mark Sweep)

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记–清除“ 算法实现的。整个过程分为4个步骤:

  • 初始标记 (CMS inital mark)
  • 并发标记 (CMS concurrent mark)
  • 重新标记 (CMS remark)
  • 并发清除 (CMS concurrent sweep)

初始标记、重新标记需要Stop The World。初始标记仅仅只是标记一下GC Roots能直接关联到的对象。速度很快。并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作儿导致标记产生的变动的哪一部分对象的标记记录,这个阶段的停顿时间一般会比初试标记阶段稍长一些,但是远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都是可以与用户线程一起工作。所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器有以下缺点:

  • CMS收集器对CPU资源非常敏感。
  • CMS收集器无法处理浮动垃圾(Floating Garbage)
  • CMS收集器基于“标记 – 清除”算法实现的,容易产生碎片。

G1收集器(Garbage first)

G1 收集器与其他GC收集器相比,具备如下特点:

  • 并发与并行:G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间。
  • 分代收集:G1可以不需要其他收集器配合就能独立管理整个GC堆。
  • 空间整合:基于“标记–整理”算法的收集器。不会产生内存空间碎片。
  • 可预测的停顿:能够建立可预测的停顿时间模型,让使用者明确指定一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代已经不再是物理隔离的了,它们都是Region的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表。每次根据允许的时间,优先回收价值最大的Region(Garbage-First名字的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器的运作大致可以划分为以下几个步骤:

  • 初始标记 (Inital Marking)
  • 并发标记 (Concurrent Marking)
  • 最终标记 (Final Marking)
  • 筛选回收 (Live Data Counting and Evacuation)

初始标记阶段仅仅只是标记以下GC Roots能直接关联到的对象。并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始对对中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可以与用户程序并发执行。而最终标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的哪一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可以并行执行。最后在筛选回首阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

三色标记 (Tri-color Marking)

三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。原理如下,

  1. 首先创建三个集合:白、灰、黑。
  2. 将所有对象放入白色集合中。
  3. 然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。
  4. 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  5. 重复 4 直到灰色中无任何对象
  6. 通过write-barrier检测对象有变化,重复以上操作
  7. 收集所有白色对象(垃圾)

golang tri-color marking

这个算法可以实现 “on-the-fly”,也就是在程序执行的同时进行收集,并不需要暂停整个程序。

但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。

reference

  • 《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》