弄明白CMS和G1,就靠这一篇了

在开始介绍CMS和G1前,咱们能够剧透几点:java

  • 根据不一样分代的特色,收集器可能不一样。有些收集器能够同时用于新生代和老年代,而有些时候,则须要分别为新生代或老年代选用合适的收集器。通常来讲,新生代收集器的收集频率较高,应选用性能高效的收集器;而老年代收集器收集次数相对较少,对空间较为敏感,应当避免选择基于复制算法的收集器。
  • 在垃圾收集执行的时刻,应用程序须要暂停运行
  • 能够串行收集,也能够并行收集。
  • 若是能作到并发收集(应用程序没必要暂停),那绝对是很妙的事情。
  • 若是收集行为可控,那也是很妙的事情。

CMS和G1做为垃圾收集器里的大杀器,是须要好好弄明白的,并且面试中也常常被问到。面试

但愿你们带着下面的问题进行阅读,有目标的阅读,收获更多:算法

  1. 为何没有一种牛逼的收集器像银弹同样适配全部场景?
  2. CMS的优势、缺点、适用场景?
  3. 为何CMS只能用做老年代收集器,而不能应用在新生代的收集?
  4. G1的优势、缺点、适用场景?

1 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。这是由于CMS收集器工做时,GC工做线程与用户线程能够并发执行,以此来达到下降收集停顿时间的目的。安全

CMS收集器仅做用于老年代的收集,是基于标记-清除算法的,它的运做过程分为4个步骤:并发

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

其中,初始标记从新标记这两个步骤仍然须要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而从新标记阶段则是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始阶段稍长一些,但远比并发标记的时间短。oracle

CMS以流水线方式拆分了收集周期,将耗时长的操做单元保持与应用线程并发执行。只将那些必需STW才能执行的操做单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就能够完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和从新标记)达到了近似并发的目的app

CMS收集器优势:并发收集、低停顿。jsp

CMS收集器缺点ide

  • CMS收集器对CPU资源很是敏感。
  • CMS收集器没法处理浮动垃圾(Floating Garbage)。
  • CMS收集器是基于标记-清除算法,该算法的缺点都有。

CMS收集器之因此可以作到并发,根本缘由在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来讲是难以接受的,所以新生代的收集器并未提供CMS版本。

另外要补充一点,JVM在暂停的时候,须要选准一个时机。因为JVM系统运行期间的复杂性,不可能作到随时暂停,所以引入了安全点的概念。

安全点(Safepoint)

安全点,即程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。

安全点的初始目的并非让其余线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便可以“安全”地执行可达性分析。只要不离开这个安全点,Java虚拟机便可以在垃圾回收的同时,继续运行这段本地代码。

程序运行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生Safepoint。

对于安全点,另外一个须要考虑的问题就是如何在GC发生时让全部线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。

两种解决方案:

  • 抢先式中断(Preemptive Suspension)

    抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。

  • 主动式中断(Voluntary Suspension)

    主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起。轮询标志的地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。

安全区域

指在一段代码片断中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也能够把Safe Region看做是被扩展了的Safepoint。

2 G1收集器

G1从新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么作的目的是在进行收集时没必要在全堆范围内进行,这是它最显著的特色。区域划分的好处就是带来了停顿时间可预测的收集模型:用户能够指定收集操做在多长时间内完成。即G1提供了接近实时的收集特性。

G1与CMS的特征对好比下:

特征 G1 CMS
并发和分代
最大化释放堆内存
低延时
吞吐量
压实
可预测性
新生代和老年代的物理隔离

G1具有以下特色:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优点,使用多个CPU来缩短Stop-the-world停顿的时间,部分其余收集器原来须要停顿Java线程执行的GC操做,G1收集器仍然能够经过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不一样,G1从总体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但不管如何,这两种算法都意味着G1运做期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会由于没法找到连续内存空间而提早触发下一次GC
  • 可预测的停顿:这是G1相对于CMS的一个优点,下降停顿时间是G1和CMS共同的关注点。

在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1将堆分红许多相同大小的区域单元,每一个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成以下图所示:

G1堆的Region布局.png

G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽可能划分2048个左右、同等大小的Region,这一点能够参看以下源码。其实这个数字既能够手动调整,G1也会根据堆大小自动进行调整。

#ifndef SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP
#define SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

#include "memory/allocation.hpp"

class HeapRegionBounds : public AllStatic {
private:
  // Minimum region size; we won't go lower than that.
  // We might want to decrease this in the future, to deal with small
  // heaps a bit more efficiently.
  static const size_t MIN_REGION_SIZE = 1024 * 1024;

  // Maximum region size; we don't go higher than that. There's a good
  // reason for having an upper bound. We don't want regions to get too
  // large, otherwise cleanup's effectiveness would decrease as there
  // will be fewer opportunities to find totally empty regions after
  // marking.
  static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;

  // The automatic region size calculation will try to have around this
  // many regions in the heap (based on the min heap size).
  static const size_t TARGET_REGION_NUMBER = 2048;

public:
  static inline size_t min_size();
  static inline size_t max_size();
  static inline size_t target_number();
};

#endif // SHARE_VM_GC_G1_HEAPREGIONBOUNDS_HPP

G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会经过一个合理的计算模型,计算出每一个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的状况下,老是能选择一组恰当的Regions做为收集目标,让其收集开销知足这个限制条件,以此达到实时收集的目的。

对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,若是发现符合以下特征,能够考虑更换成G1收集器以追求更佳性能:

  • 实时数据占用了超过半数的堆空间;
  • 对象分配率或“晋升”的速度变化明显;
  • 指望消除耗时较长的GC或停顿(超过0.5——1秒)。

原文以下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.

  • More than 50% of the Java heap is occupied with live data.
  • The rate of object allocation rate or promotion varies significantly.
  • Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)

G1收集的运做过程大体以下:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新对象,这阶段须要停顿线程,但耗时很短
  • 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段须要把Remembered Set Logs的数据合并到Remembered Set中,这阶段须要停顿线程,可是可并行执行
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。这个阶段也能够作到与用户程序一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅提升收集效率。

全局变量和栈中引用的对象是能够列入根集合的,这样在寻找垃圾时,就能够从根集合出发扫描堆空间。在G1中,引入了一种新的能加入根集合的类型,就是记忆集(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的不少开源都是源自Remembered Set,例如,它一般约占Heap大小的20%或更高。而且,咱们进行对象复制的时候,由于须要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。

image.png

卡表(Card Table)

有个场景,老年代的对象可能引用新生代的对象,那标记存活对象的时候,须要扫描老年代中的全部对象。由于该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又作全堆扫描?成本过高了吧。

HotSpot给出的解决方案是一项叫作卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,而且维护一个卡表,用来存储每张卡的一个标识位。这个标识位表明对应的卡是否可能存有指向新生代对象的引用。若是可能存在,那么咱们就认为这张卡是脏的。

在进行Minor GC的时候,咱们即可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成全部脏卡的扫描以后,Java虚拟机便会将全部脏卡的标识位清零。

想要保证每一个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机须要截获每一个引用型实例变量的写操做,并做出对应的写标识位操做。

卡表能用于减小老年代的全堆空间扫描,这能很大的提高GC效率

咱们能够看下官方文档对G1的展望(这段英文描述比较简单,我就不翻译了):

Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

3 总结

查了下度娘有关G1的文章,绝大部分文章对G1的介绍都是停留在JDK7或更早期的实现不少结论已经存在较大误差了,甚至一些过去的GC选项已经再也不推荐使用。举个例子,JDK9中JVM和GC日志进行了重构,如PrintGCDetails已经被标记为废弃,而PrintGCDateStamps已经被移除,指定它会致使JVM没法启动。

本文对CMS和G1的介绍绝大部份内容也是基于JDK7,新版本中的内容有一点介绍,倒没作过多介绍(本人对新版本JVM尚未深刻研究),后面有机会能够再出专门的文章来重点介绍。

4 参考

《深刻理解Java虚拟机》《HotSpot实战》《极客时间专栏》