新一代 GC 神器 ZGC

0. 标记-复制算法

标记-复制算法分三个阶段:

  • 标记阶段:从 GC Roots 出发,标记存活对象。
  • 转移阶段:把存活对象复制到新的内存地址,原来的内存空间变成可回收的。
  • 重定位阶段:存活对象被复制到其他地方后,所有指向对象旧地址的指针都要调整到对象新的地址上。

1. 概述

Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器。只支持 64 位的系统。

ZGC 未分代,每次 GC 都是 FullGC 。

1.1 ZGC 的设计目标

  • 亚毫秒级的最大暂停时间。
  • 暂停时间不随堆、存活对象数量、根集合大小的增长而增长。
  • 可处理的堆大小从 8MB 到 16TB。

总的来说,ZGC 具有如下特性:

  • 并发
  • 基于 Region
  • 压缩
  • NUMA 友好
  • 使用着色指针
  • 使用读屏障 load barriers

截至 JDK 14,ZGC 已在主流的操作系统上获得支持。

继续阅读

CMS GC

1. 概述

CMS,Most concurrently,是一款 采用 并发-标记-清除(Concurrent-Mark-Sweep)算法、针对老年代的 GC,其目的是最小化暂停时间。

CMS GC 在 JDK 9 被标记为 deprecated。

2. 执行过程

CMS 的标记过程采用指针可达性分析方法,对于存活的对象,在对象头上进行标记。

2.1 初始标记

从 GC Roots、新生代(Eden、From、To)出发标记直接关联的对象。

需要 STW 。

2.2 并发标记

该阶段进行 GC Root Tracing,从初始标记阶段 标记过的对象出发,标记所有可达的对象。

与应用线程并发执行,可以采用多个标记线程进行并发标记。

2.3 预清理

此阶段从新生代晋升的对象、新分配到老年代的对象以及在并发阶段修改了的对象 出发进行标记。
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的 card 标记为 dirty,通过扫描这些 card,重新标记那些在并发阶段引用被更新的对象。

通过 -XX:-CMSPrecleaningEnabled 参数选择关闭该阶段,默认开启。

目的是减少重新标记阶段的暂停时间。

CMS 提供了参数 CMSScavengeBeforeRemark 在 重新标记之前强制进行一次 minor GC,这样做的好处是缩短了重新标记阶段的暂停时间,坏处是 Minor GC 后紧跟着一个 重新标记的 暂停,这样使停顿时间也比较久。

继续阅读

GC 日志分析

不同的JVM及其选项会输出不同的日志。

GC 日志

生成下面日志使用的选项:-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:d:/GClogs/tomcat6-gc.log

4.231: [GC 4.231: [DefNew: 4928K->512K(4928K), 0.0044047 secs] 6835K->3468K(15872K), 0.0045291 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 
4.445: [Full GC (System) 4.445: [Tenured: 2956K->3043K(10944K), 0.1869806 secs] 4034K->3043K(15872K), [Perm : 3400K->3400K(12288K)], 0.1870847 secs] [Times: user=0.05 sys=0.00, real=0.19 secs] 

最前面的数字 4.2314.445 代表虚拟机启动以来的秒数。

[GC[Full GC 是垃圾回收的停顿类型,而不是区分是新生代还是年老代,如果有 Full 说明发生了 Stop-The-World 。如果是调用 System.gc() 触发的,那么将显示的是 [Full GC (System)

接下来的 [DefNew, [Tenured, [Perm 表示 GC 发生的区域,区域的名称与使用的 GC 收集器相关。
Serial 收集器中新生代名为 “Default New Generation”,显示的名字为 “[DefNew”。对于ParNew收集器,显示的是 “[ParNew”,表示 “Parallel New Generation”。 对于 Parallel Scavenge 收集器,新生代名为 “PSYoungGen”。年老代和永久代也相同,名称都由收集器决定。

方括号内部显示的 “4928K->512K(4928K)” 表示 “GC 前该区域已使用容量 -> GC 后该区域已使用容量 (该区域内存总容量) ”。

再往后的 “0.0044047 secs” 表示该区域GC所用时间,单位是秒。

再往后的 “6835K->3468K(15872K)” 表示 “GC 前Java堆已使用容量 -> GC后Java堆已使用容量 (Java堆总容量)”。

再往后的 “0.0045291 secs” 是Java堆GC所用的总时间。

最后的 “[Times: user=0.00 sys=0.00, real=0.00 secs]” 分别代表 用户态消耗的CPU时间、内核态消耗的CPU时间 和 操作从开始到结束所经过的墙钟时间。墙钟时间包括各种非运算的等待耗时,如IO等待、线程阻塞。CPU时间不包括等待时间,当系统有多核时,多线程操作会叠加这些CPU时间,所以user或sys时间会超过real时间。

堆的分代

JVM-heap-generations

在上图中:

  • young区域就是新生代,存放新创建对象;
  • tenured是年老代,存放在新生代经历多次垃圾回收后仍存活的对象;
  • perm是永生代,存放类定义信息、元数据等信息。

当GC发生在新生代时,称为Minor GC,次收集;当GC发生在年老代时,称为Major GC,主收集。 一般的,Minor GC的发生频率要比Major GC高很多。

JVM GC 相关的选项

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。