1. ZGC介绍
ZGC(The Z Garbage Collector)是 JDK 11 中推出的一款低延迟垃圾回收器,为实现以下几个目标而诞生的垃圾回收器,停顿时间不超过 10ms,停顿时间不会因堆变大而变长,支持 8MB~4TB 级别的堆(未来支持 16TB)
2. ZGC内存和原理
2.1 ZGC内存分布
ZGC 与传统的 CMS、G1 不同、它没有分代的概念,只有类似 G1 的 Region 概率;在划分内存的方式上,ZGC 与 G1 有着相似的地方,都舍弃了年轻代、老年代的划分方式,但是又与 G1 的形式不太一样,ZGC 是页为单位进行划分,一般分为三种页面:
- 小型 Region(Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
- 中型 Region(Medium Region):容量固定为 32MB,用于放置大于 256KB 但是小于 4MB 的对象。
- 大型 Region(Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中会存放一个大对象,这也预示着虽然名字叫 “大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB。大型 Region 在 ZGC 的实现中是不会被重分配的(重分配是 ZGC 的一种处理动作,用于复制对象的收集器阶段)因为复制大对象的代价非常高。
2.2 ZGC 原理
全并发的 ZGC垃圾回收流程: 与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记 - 复制算法,不过 ZGC 对该算法做了重大改进:ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于 10ms 目标的最关键原因。
ZGC 垃圾回收周期如下图所示:
ZGC 只有三个 STW 阶段:初始标记,再标记,初始转移
初始标记和初始转移分别都只需要扫描所有 GC Roots,其处理时间和 GC Roots 的数量成正比,一般情况耗时非常短;再标记阶段 STW 时间很短,最多 1ms,超过 1ms 则再次进入并发标记阶段。即,ZGC 几乎所有暂停都只依赖于 GC Roots 集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与 ZGC 对比,G1 的转移阶段完全 STW 的,且停顿时间随存活对象的大小增加而增加。
3. ZGC技术特性
3.1 着色指针
着色指针(Colored Pointers)是 ZGC 的关键技术。ZGC 之前的垃圾回收器 JVM 将对象的 GC 信息记录在对象头 Mark Word 中,GC 进行标记的时候遍历 GC Roots 的对象然后对 Mark Word 的信息进行修改。而 ZGC 将 GC 信息记录在指针中,标记算法不再寻找 Mark Word 中的信息,只需要找到相应的指针信息即可。
在 64 位架构的计算机中(ZGC 只支持 64 位), 一个 Java 对象 64 位,其中低位的 42 位是对象地址,42-45 位用来做标记信息,四个状态分别是 Marked0、Marked1、Remapped、Finalizable,剩下的 18 位预留以后使用。
创建对象时,JVM 先在堆空间申请一个内存地址,同时利用 MMAP 函数将该内存地址分别映射到 Marked0、Marked1,Remapped 完成多视图映射。在同一时间这三个视图有且仅有一个生效,这是一种 “空间换时间” 的思想。
3.2 读屏障
读屏障主要是用来解决指针在并行转移的过程中出现的问题。ZGC 进行并行转移时,GC 线程与 Java 应用线程同时工作,当 Java 应用线程读取一个未完成转移的对象的时候就会出现指针无效的问题。为了解决这个问题 ZGC 使用了读屏障的技术,当出现上述情况的时候,Java 应用线程必须在读取对象之前先把对象转移。同时 ZGC 设置了触发条件,只有在应用线程从内存堆中加载对象引用的情况下才会触发读屏障。读屏障根据着色指针记录的 GC 信息判断对象是否被移动过,如果对象发生过移动就需要对指针的内存地址进行修复。读屏障的触发条件可参考以下代码:
Object object = obj.FieldA ; // 从堆中读取对象引用,需要加入读屏障
Object o = object ; // 不需要加入读屏障,因为不是从堆中读取引用
object.doSomething (); // 不需要加入读屏障,因为不是从堆中读取引用
int i = obj.FieldB; // 不需要加入读屏障,因为不是对象引用
ZGC 中读屏障的代码作用:
GC 线程和应用线程是并发执行的,所以存在应用线程去 A 对象内部的引用所指向的对象 B 的时候,这个对象 B 正在被 GC 线程移动或者其他操作,加上读屏障之后,应用线程会去探测对象 B 是否被 GC 线程操作,然后等待操作完成再读取对象,确保数据的准确性。具体的探测和操作步骤如下:
4 JVM参数解析
4.1 JVM参数使用
下面是一些通用 GC 参数和 ZGC 特有参数以及 ZGC 的一些诊断选型,来自官网:
对比G1和ZGC JVM参数:
- JKD8 G1 的启动参数:
-server -Xms1024m -Xmx1024m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+PrintReferenceGC
-XX:+ParallelRefProcEnabled
-XX:G1HeapRegionSize=16m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/errorDump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-XX:+PrintGCApplicationConcurrentTime
-verbose:gc
-Xloggc:/opt/apps/logs/${app_name}-gc.log
- JDK 9开始部分JVM参数已移除:
CMSDumpAtPromotionFailure
CMSPrintEdenSurvivorChunks
G1LogLevel
G1PrintHeapRegions
G1PrintRegionLivenessInfo
G1SummarizeConcMark
G1SummarizeRSetStats
G1TraceConcRefinement
G1TraceEagerReclaimHumongousObjects
G1TraceStringSymbolTableScrubbing
GCLogFileSize
NumberOfGCLogFiles
PrintAdaptiveSizePolicy
PrintClassHistogramAfterFullGC
PrintClassHistogramBeforeFullGC
PrintCMSInitiationStatistics
PrintCMSStatistics
PrintFLSCensus
PrintFLSStatistics
PrintGC
PrintGCApplicationConcurrentTime
PrintGCApplicationStoppedTime
PrintGCCause
PrintGCDateStamps
PrintGCDetails
PrintGCID
PrintGCTaskTimeStamps
PrintGCTimeStamps
PrintHeapAtGC
PrintHeapAtGCExtended
PrintJNIGCStalls
PrintOldPLAB
PrintParallelOldGCPhaseTimes
PrintPLAB
PrintPromotionFailure
PrintReferenceGC
PrintStringDeduplicationStatistics
PrintTaskqueue
PrintTenuringDistribution
PrintTerminationStats
PrintTLAB
TraceDynamicGCThreads
TraceMetadataHumongousAllocation
UseGCLogFileRotation
VerifySilently
-Xloggc
- JAVA11 G1 启动参数如下:
-server -Xms1024m -Xmx1024m -Xss256k
-XX:+UseG1GC
-XX:MaxDirectMemorySize=256m
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
-XX:+SegmentedCodeCache
-verbose:gc
-XX:+PrintCommandLineFlags
-XX:+ExplicitGCInvokesConcurrent
-Djava.security.egd=file:/dev/./urandom
-Xlog:gc*,safepoint:/data/log/${app_name}-gc.log:time,uptime:filecount=100,filesize=50M
- JDK17 ZGC 的启动参数如下:
-server -Xms1024m -Xmx1024m
#开启ZGC
-XX:+UseZGC
#GC周期之间的最大间隔(单位秒)
-XX:ZCollectionInterval=120
#官方的解释是 ZGC 的分配尖峰容忍度,数值越大越早触发GC
-XX:ZAllocationSpikeTolerance=4
#关闭主动GC周期,在主动回收模式下,ZGC 会在系统空闲时自动执行垃圾回收,以减少垃圾回收在应用程序忙碌时所造成的影响。如果未指定此参数(默认情况),ZGC 会在需要时(即堆内存不足以满足分配请求时)执行垃圾回收。
-XX:-ZProactive
#GC日志
-Xlog:safepoint=trace,classhisto*=trace,age*=info,gc*=info:file=/opt/logs/gc-%t.log:time,level,tid,tags:filesize=50M
#发生OOM时dump内存日志
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/apps/errorDump.hprof
GC 日志中包含有关 GC 操作的详细信息,可以帮我们分析当前 GC 存在的问题。先来看一下上面 JVM 参数中关于 GC 日志的参数:
-
safepoint=trace:记录关于 safepoint 的 trace 级别日志。 Safepoint 是 JVM 中一个特殊的状态,它用于确保所有线程在特定操作(如垃圾回收、代码优化等)之前进入安全状态。
-
classhisto*=trace:记录与类的历史相关的 trace 级别日志。 age*=info:记录与对象年龄(在新生代中存在的时间)相关的 info 级别日志。
-
gc*=info:记录与垃圾回收相关的 info 级别日志。
-
file=/opt/logs/gc-% t.log:将日志写入到 /opt/logs/ 目录下的文件中,文件名为 gc-% t.log,其中 % t 是一个占位符,表示当前时间戳。
-
time,level,tid,tags:在每个日志记录中包含时间戳、日志级别、线程 ID 和标签。
-
filesize=50M:设置日志文件的大小限制为 50MB。当日志文件大小达到此限制时,JVM 将创建一个新的日志文件并继续记录。
更详细的 gc 日志配置可以参考:https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#enable-logging-with-the-jvm-unified-logging-framework
4.2 STW 日志解析
其中我们重点关注的就是 GC 的 STW 情况,以下是一些关键字代表 GC STW 阶段
-
最基本的 STW 三阶段,初始标记:日志中 Pause Mark Start,再标记:日志中 Pause Mark End,初始转移:日志中 Pause Relocate Start。
-
内存分配阻塞:这一般是因为垃圾生产速度大于回收速度,垃圾来不及回收,垃圾将堆占满时,线程会阻塞等待 GC 完成,关键字是 Allocation Stall(被阻塞的线程名称)
如果出现此类日志,可以尝试如下方法解决: -
-XX:ZCollectionInterval 该配置含义:两个 GC 周期之间的最大间隔(单位秒)。默认情况下,此选项设置为 0(禁用),可以适当调小该配置,让 GC 周期缩短、提升垃圾回收速度,但这会提升应用 CPU 占用。
-
-XX:ZAllocationSpikeTolerance 官方的解释是 ZGC 的分配尖峰容忍度。其实就是数值越大,越早触发回收。可以适当调大该配置,更早触发回收,提升垃圾回收速度,但这会提升应用 CPU 占用。
-
安全点:所有线程进入到安全点后才能进行 GC,ZGC 定期进入安全点判断是否需要 GC。先进入安全点的线程需要等待后进入安全点的线程直到所有线程挂起。日志关键字 safepoint … stopped
-
dump 线程、内存:比如 jstack、jmap 命令,一般是手动 dump 导致,日志关键字 HeapDumper
5 总结
ZGC 作为下一代垃圾回收器,性能非常优秀。ZGC 垃圾回收过程几乎全部是并发,实际 STW 停顿时间极短,不到 10ms。这得益于其采用的着色指针和读屏障技术。