CMS和G1

CMS

-XX:UseConcMarkSweepGC (B/S,微服务)

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程如下:

  • 初始标记(标记一下GC Roots能直接关联到的对象,素的很快)

  • 并发标记 (进行GC Roots Tracing,扩展引用链,标记阶段的主要耗时)

  • 重新标记 (修正并发标记期间因用户程序继续运作而导致标记产生变动(可能有些对象又不需要清除了)的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短)

  • 并发清除 (该阶段用户线程产生的垃圾无能为力,浮动垃圾)

​ 初始标记、重新标记这两个步骤仍然需要“stop the world” , 并发标记和并发清除过程都可以和用户线程一起工作。
CMS是一款优秀的收集器,主要优点:并发收集、低停顿。

缺点:

  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是一边标记清除,一边干活,导致cpu压力山大,使应用程序变慢,总吞吐量会降低
  • CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”,所以需要预留一段空间留给并发收集时程序运作使用,现在提升到92%(即预留8%的空间),如果清除阶段用户产生的垃圾超过了预留空间,就会导致Concurrent Mode Failure,兜底的方法是用Serial Old备份(好像被淘汰了),重新进行以full-gc,会造成一次较大的停顿。

  • CMS是一款“标记–清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。(可以预设参数设置执行多少次标记清除后进行整理,默认是0)之所以不直接采用垃圾–整理 应该是因为并发清除的阶段,若一边移动对象一边运行用户线程,可能会导致bug。

在g1之前的组合,总结两套:
CMS + ParNew :追求响应时间(用户体验)
Parallel Scavenge + Parallel Old : 追求吞吐量(后台计算)

G1收集器

-XX:UseG1GC (Java7发布)java9取代了CMS成为了默认的垃圾收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,目标是聚焦于多处理器和大容量的内存,垃圾收集的暂停时间可以更小,并且伴随着更高的吞吐量 。

同CMS相比,G1更出色的是:1、不会产生很多内存碎片。 2、在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

使用G1时,java堆的内存布局 不再是整个新生代和老年代,它将整个堆划分为多个大小相等的独立区块region,虽然还保留有新生代和老年代的概念,但他们不再是物理隔离的了(逻辑上的分代,物理上可能会随时切换),而是一部分region的集合。(化整为零 最多设置32个region,每个大小范围在1MB~32MB, 所以最大64G)

可预测的停顿时间模型:因为G1可以有计划地避免在整个java堆中进行全区域的垃圾收集,G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间/回收所需的时间, 是一个经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region(garbage-first)。这种使用region划分内存空间以及有优先级的区域回收方式,保证了G1在有限的时间内可以获取 尽可能高的收集效率。

实现:G1化整为零最大的难题在于:各个region之间不可能是孤立的,一个对象分配在某个region中,它并非只能被本region中的其他对象引用,而是可以和整个堆的任意对象发生引用关系,那么在做可达性判断时,如何确定对象是否存活,扫描整个堆?以前的收集器当然也存在该问题,但是G1更加突出。

解决: 虚拟机使用Remembered Set来避免全堆扫描,G1中每个region都有一个与之对应的Remembered Set,虚拟机发现程序在对引用类型的数据进行写操作时,会产生一个write barrier暂时中断写操作,检查引用的对象是否处于不同的region之中。(分代收集器的话就是检查是否老年代的对象引用了新生代中的对象) 如果是,便把相关的引用信息记录到被(!)引用对象所属的region的Remembered Set之中。gc时,在GC Roots的范围中加入Remembered Set即可保证不对全堆扫描。(如果要gc region1,就会查看region1的Remembered Set,上面记录了region1中的哪些对象 被别的region中的对象引用了,所以他们是存活的,至于region1中的对象引用了哪些别的region中的对象,当所有region中都有了Remembered Set,就不必全堆扫描了。

实际清理:

小区域收集+形成连续的内存块

CMS和G1
(humongous 存超大对象的区域)

不把维护Remembered Set算在内的话,G1运作步骤为:

  • 初始标记:标记GC Roots能关联到的对象,并行,但时间短
  • 并发标记:从GC Roots开始对堆中对象做可达性分析,找出存活的对象,耗时长,但并发
  • 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。,虚拟机还将这段时间的对象变化记录在线程Remembered Set Logs里面,还要把Remembered Set Logs的数据合并到Remembered Set中,这阶段要停顿,但是可以并行(快)
  • 筛选回收:对各个region的回收价值和成本进行排序,根据用户所期望的gc停顿时间来制定回收计划,本来该阶段可并发,但是因为停顿时间是用户可控制的,而且停顿用户线程将大幅度提高效率,所以不并发,并行

G1具备如下特点:

  • 并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。(逻辑上分代,物理上动态切换)

  • 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。(局部‘复制’:在具体gc某个region时(例如region1),就是普通的复制算法,将region1中存活的对象复制到别的region,然后清楚region1中所有的对象 。 整体‘标记整理’:在将region1中存活的对象复制到别的region这一步,就是标记整理,在全局的角度上,以避免内存碎片为目的,找到适合的region。)

  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超过N毫秒。

    参考资料:
    深入理解java虚拟机
    唯一图片扣自江湖id 阳哥(阳哥nb)