【金三银四】JVM虚拟机CMS和G1收集器详解

5,424 阅读10分钟

前言:

今天2B哥跟各位牛人分享JVM相关的知识点,今天重点介绍CMS和G1收集器,某些小哥哥就问为什么不讲讲其他收集器?按面试经验来说,这两种收集器问的最多,当然优先讲这两种呀,但是,我说但是,如果你关注我还能看到更多关于JVM的知识,保证让你收获满满,废话不多说,直接上干货。

图片

图片

                                       收集器于JVM堆的关系

CMS收集器

JVM参数:-XX:+UseConcMarkSweepGC

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。-XX:+UseConcMarkSweepGC来开启CMS

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

初始标记: 暂停所有的其他线程,并记录下直接与gc root 相连的对象,速度很快 ;

并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

CMS收集器测试:

图片

CMS参数配置:

-Xmx200M -Xmn50m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC

输出GC日志如下:

图片

CMS流程:

第一歩初始化标记
2020-01-05T21:52:05.411+0800: [GC (CMS Initial Mark) [1 CMS-initial-mark: 72364K(77824K)] 77550K(123904K), 0.0010600 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
第二歩并发标记
2020-01-05T21:52:05.412+0800: [CMS-concurrent-mark-start]
2020-01-05T21:52:05.416+0800: [CMS-concurrent-mark: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

第三歩 预清理
2020-01-05T21:52:05.416+0800: [CMS-concurrent-preclean-start]
2020-01-05T21:52:05.417+0800: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

第四步 可被终止的预清理
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

 第五歩 重新标记
2020-01-05T21:52:05.417+0800: [GC (CMS Final Remark) [YG occupancy: 5185 K (46080 K)]2020-01-05T21:52:05.417+0800: [Rescan (parallel) , 0.0003800 secs]2020-01-05T21:52:05.417+0800: [weak refs processing, 0.0006763 secs]2020-01-05T21:52:05.418+0800: [class unloading, 0.0009691 secs]2020-01-05T21:52:05.419+0800: [scrub symbol table, 0.0019453 secs]2020-01-05T21:52:05.421+0800: [scrub string table, 0.0004832 secs][1 CMS-remark: 72364K(77824K)] 77550K(123904K), 0.0046503 secs] [Times: user=0.02 sys=0.00, real=0.00 
 第六歩清理
2020-01-05T21:52:07.174+0800: [CMS-concurrent-sweep-start]
2020-01-05T21:52:07.175+0800: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 第七歩重置
2020-01-05T21:52:07.175+0800: [CMS-concurrent-reset-start]

2020-01-05T21:52:07.176+0800: [CMS-concurrent-reset: 0.001/0.001 secs][Times: user=0.00 sys=0.00, real=0.00 secs]

总结CMS收集器 处理过程有7个步骤:

  1. 初始标记(CMS-initial-mark) ,会导致swt;
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,会导致swt;
  6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

CMS收集器优缺点:

主要优点:并发收集、低停顿

但是它有下面三个明显的缺点:

· 对 CPU 资源敏感;

· 无法处理浮动垃圾;

· 它使用的回收算法-**“标记-清除”**算法会导致收集结束时会有大量空间碎片产生(不做整理 -XX:CMSFullGCsBeforeCompaction=n 多少次之后做压缩)。

G1收集器

JVM参数:-XX:+UseG1GC** **

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

图片

被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

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

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region**。**

一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。

G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。

默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。

Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1参数配置:

-Xmx200M -Xmn50m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseG1GC

G1收集器测试:

图片

输出日志如下:

2020-01-08T13:19:36.126+0800: [GC pause (G1 Humongous Allocation) 
第一步初始化标记
(young) (initial-mark), 0.0023830 secs]
   [Parallel Time: 1.1 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 162.8, Avg: 162.8, Max: 162.9, Diff: 0.0]
      [Ext Root Scanning (ms): Min: 0.3, Avg: 0.5, Max: 0.9, Diff: 0.6, Sum: 3.8]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.4, Max: 0.5, Diff: 0.5, Sum: 2.8]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
         [Termination Attempts: Min: 1, Avg: 7.3, Max: 11, Diff: 10, Sum: 58]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
      [GC Worker Total (ms): Min: 0.9, Avg: 0.9, Max: 0.9, Diff: 0.0, Sum: 7.2]
      [GC Worker End (ms): Min: 163.7, Avg: 163.7, Max: 163.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.6 ms]
   [Other: 0.7 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.5 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 3072.0K(50.0M)->0.0B(49.0M) Survivors: 0.0B->1024.0K Heap: 30.5M(126.0M)->28.8M(126.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-01-08T13:19:36.129+0800: [GC concurrent-root-region-scan-start]
2020-01-08T13:19:36.135+0800: [GC concurrent-root-region-scan-end, 0.0060575 secs]
第二步初始化标记
2020-01-08T13:19:36.135+0800: [GC concurrent-mark-start]
2020-01-08T13:19:36.135+0800: [GC concurrent-mark-end, 0.0000731 secs]
2020-01-08T13:19:36.135+0800: [GC remark 2020-01-08T13:19:36.135+0800: [Finalize Marking, 0.0002335 secs] 2020-01-08T13:19:36.136+0800: [GC ref-proc, 0.0000731 secs] 2020-01-08T13:19:36.136+0800: [Unloading, 0.0005882 secs], 0.0011010 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
第三步筛选回收
2020-01-08T13:19:36.137+0800: [GC cleanup 49M->49M(126M), 0.0007633 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 

G1收集器处理过程有4个步骤:

*初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;

*并发标记(Concurrent Marking):同CMS的并发标记最终标记(Remark,STW):同CMS的重新标记

*筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

G1收集器优缺点:

G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间

缺点: G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于**“复制”算法实现**的, 对内存使用率存在一定的“浪费”。

总结:

能看到这的都是牛人,麻烦帮忙点个赞关注下,下篇我继续带来CMS和G1实战PK对比,图形化对比看的更直观。

图片

          加关注,不迷路。