Serial&Parallel&CMS

156 阅读14分钟

1. 垃圾收集器发展简史

  • 1999年随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC收集器;ParNew是Serial的多线程版本;
  • 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布Parallel GC在JDK6之后成为HotSpot默认GC;
  • 2012年,在JDK1.7u4版本中,G1可用;
  • 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS;
  • 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟;
    ------------分水岭------------
  • 2018年9月,JDK11发布;引入Epsilon垃圾回收器,又被称为"No一0p(无操作)"回收器;同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)。
  • 2019年3月,JDK12发布,增强G1,自动返回未用堆内存给操作系统;同时,引入Shenandoah GC:低停顿时间的GC(Experimental);
  • 2019年9月,JDK13发布;增强ZGC,自动返回未用堆内存给操作系统;
  • 2020年3月,JDK14发布,删除CMS垃圾回收器;扩展ZGC在macOS和Windows上的应用;

2. gc的分类

对应组合的jvm-options(java 1.8中使用的option)

YoungTenuredJVM options
IncrementalIncremental-Xincgc
SerialSerial-XX:+UseSerialGC
Parallel ScavengeSerial-XX:+UseParallelGC -XX:-UseParallelOldGC
Parallel NewSerialN/A
SerialParallel OldN/A
Parallel ScavengeParallel Old-XX:+UseParallelGC -XX:+UseParallelOldGC
Parallel NewParallel OldN/A
SerialCMS-XX:-UseParNewGC -XX:+UseConcMarkSweepGC
Parallel ScavengeCMSN/A
Parallel NewCMS-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
G1-XX:+UseG1GC

查看垃圾收集器
方法1: -XX:+PrintCommandLineFlags
方法2: 使用命令行指令 jinfo -flags $pid

3. SerialGC

serialGC 在年轻代使用标记复制算法,老年代使用标记整理算法;与串行化GC名称一致,它只使用一个单独的GC线程来回收垃圾,并且无论是年轻代还是老年代都会暂停世界(STW);

串行化GC无法利用多核cpu的特性!

使用SerialGC回收垃圾

java -XX:+UseSerialGC -jar <service.jar>

SerialGC 也是有合适的使用场景的;当你的堆只有几百MB空间,而且CPU是单核的;这时候推荐使用;

为了能更好地展现GC日志,请加入如下选项!

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps

3.1 Minor GC

2023-04-01T14:45:37.987-0200<1>:151.126<2>:[GC<3>(Allocation Failure<4>) 151.126: [DefNew<5>:629119K->69888K<6>(629120K)<7>, 0.0584157 secs]1619346K->1273247K<8>(2027264K)<9>,0.0585007 secs<10>][Times: user=0.06 sys=0.00, real=0.06 secs]<11>

<1> GC 发生时间点

<2> GC事件开始的时间,相对于JVM启动时间。以秒为单位。

<3> GC的类型,minor gc 或者 full gc,此处是minor gc

<4> 回收垃圾原因 Allocation Failure eden区空间不足

<5> 垃圾回收器名称; DefNew 青年代使用的 单线程标记复制的垃圾回收器

<6> 629119K->69888K 年轻代回收前空间 和 回收后空间

<7> 629120K 整个年轻代的空间大小

<8> 1619346K->1273247K 整个堆(年轻代和老年代之和)的回收前空间 和 回收后空间

<9> 2027264K 整个堆空间大小

<10> 0.0585007 secs 垃圾回收时间,单位秒

<11> Times: user=0.06 sys=0.00, real=0.06 secs 垃圾回收整体运行时间

user jvm回收垃圾,所有线程总计使用时间

sys 操作系统调用和等待的时间

real jvm实际暂停世界的时间

这次垃圾回收时,到底有多少存活的对象?

注意将 629119K->69888K<6> 和 1619346K->1273247K<8>对比,你会计算出实际存活的对象的;

3.2 Full GC

2023-04-01T14:45:59.690-0200: 172.829:[GC (Allocation Failure) 172.829:[DefNew: 629120K->0K(629120K), 0.0000372 secs<1>]172.829:[Tenured<2>: 1203359K->755802K <3>(1398144K) <4>,0.1855567 secs<5>] 1832479K->755802K<6>(2027264K)<7>,[Metaspace: 6741K->6741K(1056768K)]<8> [Times: user=0.18 sys=0.00, real=0.18 secs]

<1> [DefNew:629120K->0K(629120K), 0.0000372 secs] 如上例,年轻代GC描述

<2> Tenured 使用标记清除-整理的单线程的老年代垃圾回收器名称;

<3> 1203359K->755802K 老年代垃圾 回收前空间 和 回收后空间

<4> 1398144K 老年代空间大小

<5> 0.1855567 secs 老年代的垃圾回收时间

<6> 1832479K->755802K 整个堆空间 回收前空间 和 回收后空间

<7> 2027264K 整个堆空间大小

<8> Metaspace: 6741K->6741K(1056768K) 元空间 回收前空间 和 回收后空间

Full GC 顾名思义,所有堆空间(young,old,metaSpace)都会进行一次垃圾回收!

4. Parallel GC

Parallel GC 在年轻代使用标记复制算法,老年代使用标记整理算法;与并发GC名称一致,它使用多个线程回收垃圾,并且无论是年轻代还是老年代都会暂停世界(STW);

使用如下参数可以控制并发的线程数

-XX:ParallelGCThreads=N

使用Parallel GC回收垃圾

java -XX:+UseParallelGC -XX:+UseParallelOldGC -jar <service.jar>

当整个堆空间较小时(几百MB),并且时多核CPU时,推荐使用此垃圾回收器;

4.1 Minor GC

2023-04-02T14:27:40.915-0200: 116.115:[GC(Allocation Failure)[PSYoungGen<1>: 2694440K->1305132K(2796544K)<2>]9556775K->8438926K(11185152K), 0.2406675 secs<3>][Times: user=1.77 sys=0.01, real=0.24 secs]

<1> PSYoungGen 使用并发标记复制的年轻代垃圾收集器名称

<2> 2694440K->1305132K(2796544K) 年轻代的描述

<3> 9556775K->8438926K(11185152K), 0.2406675 secs 整个堆的描述

4.2 Full GC

2023-04-02T14:27:41.155-0200:116.356:[Full GC (Ergonomics<1>)[PSYoungGen: 1305132K->0K(2796544K)<2>][ParOldGen:7133794K->6597672K (8388608K)<3>] 8438926K->6597672K(11185152K), [Metaspace: 6745K->6745K(1056768K)] , 0.9158801 secs, [Times: user=4.49 sys=0.64, real=0.92 secs]

<1> Ergonomics 发生GC的原因 [人体工程学]

<2> [PSYoungGen: 1305132K->0K(2796544K)] 年轻代回收描述

<3> [ParOldGen:7133794K->6597672K (8388608K)] 老年代回收描述

Parallel Scavenage GC提供两个参数 -XX:MaxGCPauseMillis 与 -XX:GCTimeRatio 自动调整堆大小与其他与GC相关的参数,达到GC调优的目的;

关于人体工程学的额外解释

-XX:+UseAdaptiveSizePolicy

吞吐量垃圾收集器提供了一个有趣的(但常见,至少在现代JVM上)机制-人体工程学,以提高垃圾收集配置的用户友好性。 通过人体工程学,垃圾收集器能将动态调整堆大小,从而能提高GC性能。“提高GC性能”的确切含义可以由用户通过-XX:GCTimeRatio和-XX:MaxGCPauseMillis(见下文)指标来指定。
人体工程学默认是激活的。自适应同样是是JVM最大优势之一。 在某些情况下,我们可能不希望JVM修改我们对堆的设置。 可以通过-XX:-UseAdaptiveSizePolicy停用自适应调整。

-XX:GCTimeRatio=nnn

表示希望在GC花费不超过应用程序执行时间的1/(1+nnn),nnn为大于0小于100的整数。

换句话说,此参数的值表示运行用户代码时间是GC运行时间的nnn倍。

举个官方的例子,参数设置为19,那么GC最大花费时间的比率=1/(1+19)=5%,程序每运行100分钟,允许GC停顿共5分钟,其吞吐量=1-GC最大花费时间比率=95%

-XX:MaxGCPauseMillis

通过**-XX:MaxGCPauseMillis**=表示JVM最大暂停时间的目标值(以毫秒为单位)。默认没有设置值;为了保持低暂停时间,JVM会增加GC次数,这会严重影响到的吞吐量

5. CMS(ConcurrentMarkSweepGC)

ConcurrentMarkSweepGC 由名字可知,尽可能地并发标记,然后进行垃圾清除;年轻代,它使用多个并发线程进行标记复制;老年代,使用标记清除算法;

设计CMS的目的是为了减少在老年代垃圾回收的暂停时间;第一点,垃圾回收之后不再整理空间,而是通过free-List管理内存;第二点,它在标记-清除阶段与应用程序同时完成大部分工作(允许GC线程和用户线程并行执行)。

使用CMS回收垃圾

java -XX:+UseConcMarkSweepGC -jar <service.jar>

5.1 Minor GC

2023-04-03T16:23:07.219-0200: 64.322:[GC(Allocation Failure) 64.322: [ParNew<1>: 613404K->68068K6(613440K), 0.1020465 secs] 10885349K->10880154K (12514816K), 0.1021309 secs][Times: user=0.78 sys=0.01, real=0.11 secs]

<1> ParNew 使用并发标记复制的年轻代垃圾收集器名称

5.2 Full GC

我们重点关注CMS,回收老年代垃圾的过程

2023-04-03T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

2023-04-03T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]

2023-04-03T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]

2023-04-03T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]

2023-04-03T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

2023-04-03T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]

2023-04-03T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]

2023-04-03T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

2023-04-03T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]

2023-04-03T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

2023-04-03T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]

2023-04-03T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

CMS的日志是分成很多个阶段

5.2.1 初始标记

初始标记(CMS的第一个STW阶段),标记GC-Root直接引用的对象,GC-Root直接引用的对象不多,所以很快;

初始标记时,年轻代也是需要扫描的;

2023-04-03T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs <1>] [Times: user=0.00 sys=0.00, real=0.00 secs]

<1>[1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs]

10812086K(11901376K) 当前已使用的老年代空间(老年代空间大小)

10887844K(12514816K) 当前已经使用的堆空间大小(堆空间大小)

0.0001997 初始标记耗时

5.2.2 并发标记

并发标记阶段,由第一阶段标记过的对象出发,所有可达的对象都在本阶段标记;

重点,上一小结说过多标现象;

在并发标记阶段 和 标记清除阶段,都会有新加入的对象;只有在此阶段加入的对象,都认为其和GC-root关联!

若因为多标,进而产生过多的浮动垃圾,会导致标记失败,从而使用Serial-GC进行强制Full-GC

2023-04-03T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]

2023-04-03T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]

<1> 0.035 secs 并发标记耗时

5.2.3 并发预清理

并发预清理阶段,也是一个并发执行的阶段;在本阶段,会查找前一阶段执行过程中,从新生代晋升或新分配或被更新的对象;通过并发地重新扫描这些对象,预清理阶段可以减少下一个stop-the-world 重新标记阶段的工作量;

当前一个阶段与应用程序并发运行时,一些引用被更改了,JVM会将包含突变对象的堆区域(称为“Card”)标记为“脏”(这称为Card Marking)。(也就是上一章,提到的card-table);

标脏的过程,实际上就是 CMS的增量更新;

在预清理阶段,GC会处理所有 dirty-card,知道所有card,全部变为干净的(突变对象被重新扫描)

22023-04-03T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]

2023-04-03T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs <1>] [Times: user=0.02 sys=0.00, real=0.02 secs]

5.2.4 并发可中止的预清理阶段

这个阶段其实跟上一个阶段做的事情一样,也是为了减少下一个STW重新标记阶段的工作量;增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认5秒)或者Eden区使用占比达到期望比例(默认50%)就结束本阶段;

2023-04-03T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]

2023-04-03T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]

5.2.5 最终标记阶段

CMS的第二个STW阶段,暂停所有用户线程,从GC-Root开始重新扫描整堆,标记存活的对象;需要注意的是,虽然CMS只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多GC-Root都在新生代;

CMS 最终标记阶段,还需要再次扫描一次GC-Root,以防止漏标!

2023-04-03T16:23:08.447-0200:

65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K) <1>]

65.550: [Rescan (parallel) , 0.0085125 secs <2>]

65.559:[weak refs processing, 0.0000243 secs <3>]

65.559:[class unloading, 0.0013120 secs <4>]

65.560:[scrub symbol table, 0.0008345 secs <5>]

65.561:[scrub string table, 0.0001759 secs <6>]

[1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs <7>]

[Times: user=0.06 sys=0.00, real=0.01 secs]

<1> YG occupancy: 387920 K (613440 K) 年轻代已用空间(可用空间)

<2> Rescan (parallel) , 0.0085125 secs 重新扫描的耗时

<3> weak refs processing, 0.0000243 secs 弱引用,GC时主动回收

<4> class unloading, 0.0013120 secs 类的卸载

<5> scrub symbol table, 0.0008345 secs 类元信息清理

<6> scrub string table, 0.0001759 secs 字符串常量池清理

<7> CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs

10812086K(11901376K) 当前已使用的老年代空间(老年代空间大小)

11200006K(12514816K) 当前已经使用的堆空间大小(堆空间大小)

5.2.6 标记清除阶段

标记完成,回收垃圾!

2023-04-03T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]

2023-04-03T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

5.3 CMS相关参数

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS-GC前启动一次minor-gc,目的在于减少老年代对年轻代的引用,降低CMS-GC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;