CMS和G1详解

 

首先

1.G1是包括年轻代和年老代的GC

2.CMS是年老代GC

3.二者在某些时候都需要FullGC(serial old GC)的辅助

 

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的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。

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

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

CMS收集器缺点

  • 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收集器

分为 : YGC (复制算法 stw  利用卡表)、MixedGC(RSet  漏标)

触发Full GC,类似CMS也会触发FullGC【serial old GC,所以本质上,G1和CMS是不提供也不用提供Full GC】(在对象过大找不到可存放的连续H区时才会进行FullGC或者Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(Full GC)来收集整个GC heap。)

不建议手动指定年轻代的大小,默认是一个活动范围,可以指定年轻代的stw的时间,这样G1会动态调整年轻代大小以满足每次STW的时间需要

G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。G1 的主要关注点在于达到可控的停顿时间,在这个基础上尽可能提高吞吐量。

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收集器将整个Java堆划分为多个大小相等的独立区域(Region)。Region是一块地址连续的内存空间,G1模块的组成如下图所示:

G1堆的Region布局.png

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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)

Mixed GC中的全局并发标记  global concurrent marking:

  • global concurrent marking的执行过程类似于CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。
  • global concurrent marking的执行过程分为四个步骤。
  • 初始标记(inital mark,STW):它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking):这个阶段从GC Root开始对heap中的对象进行标记,标记线程与应用程序线程并发执行,并且收集各个Region的存活对象信息。
  • 重新标记(Remark,STW):标记那些在并发标记阶段发生变化的对象【利用的是白会黑三色标记防止漏标】,将被回收。
  • 清理(Cleanup):清除空Region(没有存活对象的),清空后的Region加入到free list。
  • 第一阶段inital mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
  • 第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。

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

image.png

G1 比 ParallelOld 和 CMS 会需要更多的内存消耗,那是因为有部分内存消耗于簿记(accounting)上,如以下两个数据结构:

  • Remembered Sets:每个区块都有一个 RSet,用于记录进入该区块的对象引用(如区块 A 中的对象引用了区块 B,区块 B 的 Rset 需要记录这个信息),它用于实现收集过程的并行化以及使得区块能进行独立收集
  • Collection Sets:将要被回收的区块集合。GC 时,在这些区块中的对象会被复制到其他区块中,总体上 Collection Sets 消耗的内存小于 1%。

卡表(Card Table)

主要优化YGC过程

有个场景,在YGC的时候,老年代的对象可能引用新生代的对象,那在标记年轻代存活对象的时候,就需要扫描老年代中的所有对象才能确定那些该回收那些不该回收。那不是得又做全堆扫描?成本太高了吧。

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.

 漏标问题以及G1是如何解决的:

三色标记(白灰黑)配合收集在并行标记时哪些白色标记对象存在引用指向发生变化了的集合,在Remark阶段予以标记

3 ZGC

ZGC(Z Garbage Collector)作为一种比较新的收集器,目前还没有得到大范围的关注。作为一款低延迟的垃圾收集器,它有如下几个亮点:

  • 停顿时间不会超过 10ms
  • 停顿时间不会随着堆的增大而增大(控制停顿时间在10ms内)
  • 支持堆的大小范围很广(8MB-16TB)

在ZGC中,连逻辑上的也是重新定义了堆空间(不区分年轻代和老年代),只分为一块块的page,每次进行GC时,都会对page进行压缩操作,所以没有碎片问题。虽然ZGC属于很新的GC技术, 但优点不一定真的出众,ZGC只在特定情况下具有绝对的优势, 如巨大的堆和极低的暂停需求。而实际上大多数开发在这两方面都不太成问题(尤其是在服务器端), 而对GC的性能/效率更在意。也有一种观点认为ZGC是为大内存、多cpu而生,它通过分区的思路来降低STW。

ZGC在JDK14前只支持Linux, 从JDK14开始支持Mac和Windows。

可以从官网看下ZGC的Change Log:

  • JDK 15 (under development)
    • Improved NUMA awareness
    • Support for Class Data Sharing (CDS)
    • Support for placing the heap on NVRAM
  • JDK 14
    • macOS support (JEP 364)
    • Windows support (JEP 365)
    • Support for tiny/small heaps (down to 8M)
    • Support for JFR leak profiler
    • Support for limited and discontiguous address space
    • Parallel pre-touch (when using -XX:+AlwaysPreTouch)
    • Performance improvements (clone intrinsic, etc)
    • Stability improvements
  • JDK 13
    • Increased max heap size from 4TB to 16TB
    • Support for uncommitting unused memory (JEP 351)
    • Support for -XX:SoftMaxHeapSIze
    • Support for the Linux/AArch64 platform
    • Reduced Time-To-Safepoint
  • JDK 12
    • Support for concurrent class unloading
    • Further pause time reductions
  • JDK 11
    • Initial version of ZGC
    • Does not support class unloading (using -XX:+ClassUnloading has no effect)

可以看出ZGC未来可期,让我们拭目以待吧。

你可能感兴趣的:(CMS和G1详解)