通俗易懂 JVM 中的 GC 实现

在上篇文章中介绍了Java GC:基础原理,这篇文章来看下在 JVM 中是如何实现具体的收集器的。git

JVM 提供了多种垃圾收集器用来分别收集新生代和老年代,新老收集器之间能够组合使用,可是在实际使用中(基于Java 8),通常常见的有4种收集器组合github

  • Serial GC 收集新生代和老年代 -XX:+UseSerialGC
  • Parallel GC 收集新生代和老年代(JVM默认使用的组合) -XX:+UseParallelGC(该参数包含了-XX:+UseParallelOldGC)
  • Parallel New 收集新生代 + Concurrent Mark and Sweep (CMS) 收集老年代 -XX:+UseConcMarkSweepGC(该参数包含了 -XX:+UseParNewGC)
  • G1 (不严格区分新老代)-XX:+UseG1GC-

PS: 在 Java 8 中 -XX:+UseParallelOldGC 和 -XX:+UseParNewGC 已经没法单独使用算法

下面咱们从原理上去理解每一个收集器是如何工做的,但并不会去细究具体的实现。数据结构

Serial GC

Serial GC 使用 mark-copy 算法处理新生代,使用 mark-sweep-compact 算法处理老年代。正如名字所说的那样,这是一个单线程的收集器。整个收集过程会触发 stop-the-world 暂停应用,直到回收结束。该收集器适用于只有几百M大小的堆和单核 CPU的状况。对于服务端,不多会使用这个组合,由于没法合理的使用计算机资源,不过反过来讲的话,这也能够知足对系统资源使用有限制的状况。多线程

Parallel GC

Parallel GC 和 Serial GC 使用的算法同样。只不过是多线程的,是 JVM 默认使用的 GC 。和 Serial GC 同样,在整个收集过程会触发 stop-the-world 暂停应用,直到回收结束。在多核环境下,处理速度要比 Serial GC 快,能够提升吞吐量。并发

Concurrent Mark and Sweep(CMS)

Serial GC 和 Parallel GC 在回收垃圾的过程当中须要暂停应用线程,所以若是对应用延迟有要求的话,CMS 收集器是一个更好的选择。其使用 stop-the-world 的 mark-copy 算法收集新生代(ParNew,和 Parallel GC 类似但不兼容,只和 CMS 一块儿使用),使用几乎和应用线程并发的 mark-sweep 算法收集老年代。post

PS :至于 ParNew 和 Parallel 的关系是有历史缘由的,有兴趣的能够看这个为何有Parallel GC 了,还要有个 ParNew,还不兼容?线程

CMS 主要用于避免老年代回收时长时间的暂停。首先,其使用了 mark-sweep 算法而不进行压缩(注意因为会产生内存碎片,所以在内存分配上使用了 free-lists 而不是指针碰撞,因此分配速度会相对慢一些) 。其次,在 mark-sweep 算法收集过程当中,与应用程序几乎是并发的,几乎不影响应用程序的执行(暂停的时间很短)。设计

PS:CMS为何没有采用标记-整理算法来实现?3d

不过凡事有利即有弊

  • 因为大多数时候 CMS 至少使用了一些 CPU 资源而没有执行应用程序的代码,所以 CMS 的吞吐量一般要比 Parallel GC 差。
  • 因为 sweep 后没有进行 compact,必然会形成内存碎片的问题(JVM 提供了参数能够设置在一次或屡次 Full GC 后对内存进行整理)。
  • 因为存在和应用程序并发的阶段,不能和其余收集器同样,等到老年代放不下了才作回收,须要设定一个阈值。若是在并发过程当中,老年代满了的话,则会触发 “Concurrent Mode Failure” 错误,说明并发过程失败,会 stop-the-world,执行 Full GC,等到回收完毕后才恢复应用线程。
  • 会存在浮动垃圾,即标记存活后死亡的对象,须要在下一次 GC 中才能清理。

CMS 的并发收集过程以下:

  1. 初始标记(Initial Mark) 初始标记是一个 stop-the-world 的过程,不过该过程只须要标记老年代中全部 GC Roots 可直接抵达的对象,时间很是短。
  2. 并发标记(Concurrent Mark) 在不暂停用户线程的状况下,并发的标记存活对象。此时因为应用线程还在运行,可能会有一些新的对象进入到老年代,或者以前的对象在标记后发生一些引用的变化。在以前介绍的基础算法中的内容提到过 JVM 使用了 Card Marking 的技术来标记老年代对新生代的引用;这里也使用了相同的技术,并发过程当中产生的引用变化也使用了一个和 CardTable 相似的结构(ModUnionTable)记录下引用变化的对象。
  3. 预清理阶段 (Concurrent Preclean) 这个阶段须要处理上个阶段遗留的问题,遍历 Ditry Card 中对象从新标记一遍存活对象。不过,对于以前标记存活但如今已经死亡的垃圾对象就须要留到下一次清理了(浮动垃圾)。这个阶段是能够关闭的。
  4. 可中断预清理阶段 (Concurrent Abortable Preclean) 重复执行上一个阶段的操做,直到知足某种条件,能够是循环次数达到阈值或者循环处理时间达到阈值等等。该阶段的目的在于尽量的去处理那些在并发阶段被应用线程更新的老年代对象,以减小从新标记阶段的暂停时间。
  5. 最终从新标记 (Final Remark). 只要还在并发,就永远存在新的引用关系没有处理,所以须要整个过程当中第二个也是最后一个 stop-the-world 的阶段来确保全部存活的对象都被标记。
  6. 并发清除 (Concurrent Sweep) 因为不须要 compact,没有 stop-the-world 的必要,直接清除就行了。CMS会维护本身的空闲列表,不会把在这个阶段中新晋升上来的对象给清除了。
  7. 并发重置 (Concurrent Reset) 重置内部的数据结构以便下一次使用

此外,除了被动的由 Mionr GC触发 Old GC 外,在实际应用中,还存在着主动的,周期性的 Old GC,只不过存在着触发条件,有兴趣的能够自行额外查阅。

虽然 JDK 9 已经废除 CMS 垃圾收集器,但 CMS 仍然是 JDK 8 及之前的追求低延迟的不错的可选项 。

G1 - Garbage First

为解决 CMS 算法产生的一系列问题缺陷,HotSpot提供了另一种垃圾回收策略,从 JDK 9 开始,G1 取代 Parallel GC 成为 JVM 中的默认 GC。G1 的其中一个核心目标是使 stop-the-world 的时间可预测而且可配置。好比说,你能够要求每分钟 stop-the-world 的暂停时间不能超过 5 毫秒,G1 会尽最大可能去完成需求,但并非必定能达到的。

不像以前的堆内存是按分代连续分布的,G1 将堆划分红了必定数量(典型的如2048个)的小区域(Region),每一个小区域多是 Eden, Survior 或者 Old 区域。在逻辑上,Eden 和 Survivor 区域组成了新生代,Old 区域组成了老年代。这个设计容许 GC 每一个只收集其中的一些区域而不是整个堆。不过新生代区域每次都会参与。另外一个比较新奇的特色是 G1 能够估计每一个区域存活对象的多少,存活数量多的区域会先被收集,这也是这个收集器名字的由来,garbage first 收集器。

G1 大体可分为 Young GC 和 Mixed GC,Young GC 处理全部的 Young Region, Mixed GC 处理全部的 Young Region 和 部分的 Old Region,至于选择哪些 Old Region,须要并发标记来搜集必要的信息。分代式G1的正常工做流程就是在 young GC 与 mixed GC 之间视状况切换,背后按期作并发标记搜集资料。

Young GC —— 只回收年轻代的疏散阶段 (Evacuation Pause: Fully Young)

在应用程序刚开始的时候,G1 没有额外的信息去运行并发阶段。所以在这个阶段,G1 的运做模式和其余的新生代收集器很像,须要 stop-the-world,使用复制算法,将存活对象的复制到 Survivor 区域中,或者空闲区域中(以后这个区域也是 Survivor 区)。该阶段能够理解为 Young GC (Minor GC),所谓疏散,其实就是 copy 到其余区域中。

并发标记 (Concurrent Marking)

G1 收集器的不少概念是创建在 CMS 上的,所以在流程上会有一些类似。尽管如此,它们之间也有不少不同的地方,好比 CMS 则是采用增量更新的方式,即额外 mark 修改的引用再作处理,而 G1 使用了 SATB (Snapshot-At-The-Beginning) 来维持并发 GC 的正确性 。Snapshot-At-The-Beginning,从字面上理解,就是在 GC 开始时产生一个全部存活对象的逻辑快照,在这个快照中存活的对象,加上以后在 GC 过程当中新分配的对象认为是最终的存活对象,其它不可到达的对象就是死的了。关于 SATB 具体是如何工做的,能够看R大写过的 G1 讲解

和 CMS 同样,该阶段具体的能够分为如下几个阶段,每一个阶段的目的和 CMS 差很少,只是具体实现上会有区别:

  1. 初始标记(Initial Mark)
  2. 并发标记(concurrent marking)
  3. 最终标记(remark)
  4. 清理(cleanup)

其中 Initial Mark 在 Evacuation Pause 中捎带完成了。注意这里的 cleanup,并非真的在堆上清理了实际对象,而是统计每一个 region 存活数量的多少,并按预期的 GC 效率对它们进行排序,为 mixed GC 作准备。但有例外,若是发现这个 region 都没有存活对象,整个区域会被回收到可分配的 region 列表中。

Mixed GC —— 混合的疏散阶段 (Evacuation Pause: Mixed)

这个阶段并不必定紧跟在并发标记以后,须要知足必定的条件,好比,若是能够同时释放一大部分的老年代,不然该阶段没有必要。所以,在并发标记结束和混合疏散暂停之间可能很容易出现一些只回收年轻的疏散阶段。

该阶段选定全部新生代里的 region,外加根据并发标记统计得出收集收益高的若干个老年代 region(在用户指定的开销目标范围内尽量选择收益高的old gen region)进行回收。

相关文章
相关标签/搜索