经典垃圾回收器和现代垃圾回收器
CMS 及以后的垃圾回收器被称为"现代垃圾回收器",CMS 之前的通常称为"传统垃圾回收器"或"经典垃圾回收器"。
为什么这样区分?
1. 并发与低停顿
- 传统垃圾回收器(如 Serial GC、Parallel GC)在回收时会长时间 Stop-The-World(STW),所有应用线程暂停,影响响应速度。
- 现代垃圾回收器 (从 CMS 开始,如 CMS、G1、ZGC、Shenandoah)引入了并发标记、并发清理等机制,GC 线程和应用线程可以同时工作,大大降低了停顿时间,提升了应用的响应性。
2. 适应多核和大堆
- 现代 GC 更好地支持多核 CPU 和大内存堆,能在大规模服务端场景下保持良好性能。
3. 更智能的算法
- 现代 GC 引入了三色标记法、SATB、分区回收、预测停顿等更智能的算法和机制,能更好地平衡吞吐量与延迟。
总结
- CMS 之前:Serial GC、Parallel GC,称为"传统"或"经典"垃圾回收器,特点是全停顿、简单高效但不适合低延迟场景。
- CMS 及以后:CMS、G1、ZGC、Shenandoah,称为"现代垃圾回收器",特点是并发、低停顿、适合大堆和多核环境。
本质区别 :
现代垃圾回收器的目标是降低应用停顿时间,提高响应性和可扩展性,而传统垃圾回收器更关注实现简单和吞吐量。
ZGC(Z Garbage Collector)
一、ZGC 简介
ZGC (Z Garbage Collector)是 Oracle/OpenJDK 推出的可扩展、低延迟 的垃圾回收器,目标是在任意堆大小下(数百 MB 到数 TB) ,将 GC 停顿时间控制在10 毫秒以内。ZGC 适合对延迟极其敏感、堆内存极大的应用场景。
- JDK11 引入实验性支持,JDK15 起正式可用。
- 启用参数:
-XX:+UseZGC
二、ZGC 的运行机制与原理
1. 堆结构
- ZGC 将堆划分为多个等大小的 Region,每个 Region 至少 2MB。
- Region 类型有:小对象区(Small)、大对象区(Medium/Large)、Remap 区等。
2. 并发回收,极短停顿
- ZGC 的所有 GC 阶段几乎都是并发的,只有极少数阶段会 Stop-The-World(通常低于 1ms)。
- 应用线程和 GC 线程几乎全程并发运行。
3. 三色标记法与染色指针(Colored Pointers)
- ZGC 采用三色标记法,并通过**染色指针(Colored Pointers)**技术,在对象引用的高位嵌入元数据(如标记状态、转发状态)。
- 这样可以在不暂停应用线程的情况下,安全地移动和标记对象。
4. GC 主要阶段
ZGC 的一次回收分为以下阶段:
1)Mark Start(初始标记,STW)
- 极短暂停,标记 GC Roots 直接可达对象。
2)Concurrent Mark(并发标记)
- 与应用线程并发,遍历对象图,标记所有可达对象。
3)Relocate Start(准备重定位,STW)
- 极短暂停,准备对象搬迁。
4)Concurrent Relocate(并发重定位)
- 与应用线程并发,将存活对象搬迁到新 Region,更新引用。
5)Concurrent Remap(并发修正)
- 修正应用线程在搬迁期间产生的引用变更。
6)Reset(重置)
- 重置内部数据结构,为下一次 GC 做准备。
5. 读屏障(Load Barrier)
- ZGC 采用读屏障(Load Barrier) ,每次应用线程读取对象引用时,都会检查引用状态(是否已搬迁、是否需要修正)。
- 这样即使对象在 GC 过程中被移动,应用线程也能安全访问。
6. 并发处理引用和 Finalizer
- ZGC 对软/弱/虚引用、Finalizer 的处理也都是并发完成,进一步降低停顿。
三、ZGC 的优势
- 极低停顿:GC 停顿时间通常低于 1ms,几乎与堆大小无关。
- 超大堆支持:可支持 TB 级堆内存。
- 全并发:标记、搬迁、清理等几乎全并发。
- 无碎片:对象搬迁时整理内存,避免碎片。
- 无需复杂调优:参数简单,自动适应大多数场景。
四、ZGC 的局限
- 需要 64 位操作系统和 CPU 支持指针染色。
- JDK11 及以上版本才支持。
- 目前不支持 32 位 JVM 和部分老旧平台。
五、ZGC 启动参数示例
ruby
-XX:+UseZGC
-XX:MaxGCPauseMillis=10
-XX:+UnlockExperimentalVMOptions # JDK11/12 需要
六、总结
- ZGC 是目前 Java 世界中延迟最低、可扩展性最强的垃圾回收器之一。
- 通过 Region 划分、染色指针、读屏障和全并发设计,实现了极低的 GC 停顿和超大堆支持。
- 适合对延迟极敏感、内存极大的服务端、金融、在线交易等场景。
染色指针和读屏障
一、染色指针(Colored Pointers)
1. 基本概念
- 染色指针是指在对象引用(指针)的高位嵌入额外的元数据(称为"颜色"),用于记录对象的GC状态。
- 这些"颜色"不是颜色本身,而是用几位二进制位来标记对象的不同状态,比如"已标记"、"已转发"、"需要修正"等。
2. 染色指针的作用
- 允许GC在不暂停应用线程的情况下,安全地移动对象和更新引用。
- 通过指针的高位,GC和应用线程可以快速判断对象的当前状态。
3. 染色指针的实现
-
64位操作系统下,虚拟地址空间远大于实际物理内存,指针的高位通常未被使用。
-
ZGC等GC利用这些未用的高位,嵌入元数据(如2~4位),实现"染色"。
-
例如:
- 00:普通引用
- 01:已标记
- 10:已转发(对象已搬迁)
- 11:需要修正
4. 染色指针的优势
- 不需要额外的对象头或全局表,节省内存和提升效率。
- 支持并发GC和对象移动时的引用透明性。
二、读屏障(Load Barrier)
1. 基本概念
- 读屏障是一种在读取对象引用时自动执行的检查或修正逻辑。
- 在 ZGC 这样的并发移动式 GC 中,应用线程每次读取对象引用时,都会通过读屏障判断引用的状态。
2. 读屏障的作用
- 保证应用线程在GC过程中,始终能访问到对象的最新、正确位置。
- 如果对象已被搬迁,读屏障会自动将引用修正为新地址。
- 如果对象需要标记或其他处理,读屏障也会自动完成。
3. 读屏障的实现方式
-
读屏障通常由JVM自动插入到每次对象引用的读取操作中(JIT编译器支持)。
-
读屏障会检查指针的染色位,根据不同状态执行不同逻辑:
- 如果是普通引用,直接返回。
- 如果是已转发,自动修正引用为新地址。
- 如果需要标记,自动完成标记。
4. 读屏障的优势
- 允许GC和应用线程几乎全程并发,极大降低STW停顿。
- 保证对象移动和引用修正的透明性和安全性。
三、二者协同工作示意
- GC 线程在并发标记/搬迁对象时,更新对象引用的染色位。
- 应用线程读取对象引用时,读屏障检查染色位,自动修正引用或完成标记。
- 整个过程无需长时间暂停应用线程,实现极低延迟的垃圾回收。
四、总结
- 染色指针:在指针高位嵌入元数据,记录对象GC状态。
- 读屏障:每次读取引用时自动检查和修正,保证对象移动和GC的并发安全。
- 这两项技术是 ZGC、Shenandoah 等现代低延迟GC实现"几乎无停顿"回收的核心。
日志分析、调优建议
一、ZGC 日志样例与分析
ZGC 的 GC 日志格式与其他 GC 不同,更加结构化和易读。常见日志片段如下:
scss
[2024-06-16T10:00:00.123+0800][info][gc,start ] GC(0) Pause Init Mark (G1 Humongous Allocation)
[2024-06-16T10:00:00.124+0800][info][gc ] GC(0) Pause Init Mark 0.123ms
[2024-06-16T10:00:00.124+0800][info][gc,start ] GC(0) Concurrent Mark
[2024-06-16T10:00:00.130+0800][info][gc ] GC(0) Concurrent Mark 6.123ms
[2024-06-16T10:00:00.130+0800][info][gc,start ] GC(0) Pause Relocate Start
[2024-06-16T10:00:00.131+0800][info][gc ] GC(0) Pause Relocate Start 0.234ms
[2024-06-16T10:00:00.131+0800][info][gc,start ] GC(0) Concurrent Relocate
[2024-06-16T10:00:00.140+0800][info][gc ] GC(0) Concurrent Relocate 9.123ms
[2024-06-16T10:00:00.140+0800][info][gc ] GC(0) Garbage Collection (0) Complete
日志阶段说明
- Pause Init Mark:初始标记,STW,极短暂停(通常小于1ms)。
- Concurrent Mark:并发标记,应用线程和GC线程并发,耗时最长。
- Pause Relocate Start:准备对象搬迁,STW,极短暂停。
- Concurrent Relocate:并发搬迁对象,应用线程和GC线程并发。
- Complete:本次GC完成。
- GC(0) :这是 JVM 启动后发生的第 0 次 GC。每发生一次 GC,这个数字会递增(如
GC(1)
、GC(2)
等),用于区分和追踪每一次垃圾回收的全过程。
重点关注
- STW阶段耗时(Pause Init Mark、Pause Relocate Start):应极短,通常小于1ms。
- Concurrent阶段耗时:与应用线程并发,耗时长但不影响响应。
- GC频率:过于频繁说明内存压力大或参数需调整。
- 是否有"Allocation Stall" :表示分配速度过快,GC来不及回收,需关注。
二、ZGC 调优建议
1. 设置最大停顿时间
ini
-XX:MaxGCPauseMillis=10
ZGC 会尽量将单次GC停顿控制在10ms以内(默认1ms~10ms)。
2. 合理设置堆大小
- 堆越大,GC越高效,但内存占用也高。
- ZGC 支持超大堆(TB级),但建议根据实际业务负载设置。
3. 关注 Humongous Allocation
- 如果日志频繁出现
Humongous Allocation
,说明有大量大对象分配,建议优化代码,减少大对象直接分配。
4. 监控 Allocation Stall
- 如果日志出现
Allocation Stall
,说明分配速度过快,GC来不及回收,建议加大堆或优化对象生命周期。
5. 开启详细日志
ruby
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
-XX:MaxGCPauseMillis=10
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xlog:gc*:file=gc.log:time,uptime,level,tags
6. 观察并发阶段耗时
- 并发阶段耗时长一般不是问题,但如果GC频率高,说明内存压力大。
7. 关注 Full GC
- ZGC 极少 Full GC,若出现需重点关注,通常是内存不足或大对象过多。
三、总结
- ZGC 日志重点关注 STW 停顿时间、GC频率、Humongous Allocation 和 Allocation Stall。
- 调优主要围绕堆大小、最大停顿时间和大对象分配优化。
- ZGC 一般无需复杂调优,参数简单,适合对延迟极敏感的场景。
元空间的回收
ZGC 极少发生 Full GC ,而元空间(Metaspace)的回收确实是在 Full GC 阶段进行的 。
这意味着:
- 如果 ZGC 很少或几乎不发生 Full GC,那么元空间的垃圾(比如卸载的类、ClassLoader 相关的元数据)也会很少被回收。
- 这可能导致元空间占用持续增长,直到触发一次 Full GC,才会释放这些元空间的垃圾。
实际影响:
- 对于类动态加载/卸载频繁的应用(如某些容器、插件系统),如果 ZGC 很少 Full GC,元空间可能会积累较多垃圾。
- 但大多数普通应用,元空间增长不是主要瓶颈。
调优建议:
- 可以通过监控元空间使用情况(如 JMX、jstat、GC 日志)来判断是否有泄漏或积压。
- 如确实需要回收元空间,可以手动触发 Full GC(如
System.gc()
),但一般不推荐频繁这样做。
总结:
ZGC 极少 Full GC,元空间的垃圾回收也会变得极少。如果你的应用对元空间回收有特殊需求,需要特别关注和监控。
ZGC 为什么没有清除阶段
1. ZGC 的对象回收机制
- ZGC 在**并发搬迁(Concurrent Relocate)**阶段,会把存活对象从旧 Region 搬迁到新 Region。
- 未被搬迁的对象(即垃圾)所在的 Region,在搬迁完成后会被整体回收和复用。
- 也就是说,ZGC 通过 Region 的整体复用来实现垃圾的清理,而不是像 CMS 那样遍历并清除每个垃圾对象。
2. 为什么没有单独的清除阶段?
- ZGC 的 Region 设计,使得只要 Region 里的对象都被搬迁走,这个 Region 就可以直接复用,无需单独清扫。
- 这样可以避免传统 Sweep 阶段的 STW 或并发遍历,提升并发性和效率。
3. 总结
ZGC 没有传统意义上的"清除(Sweep)"阶段,是因为它通过并发搬迁和 Region 复用来实现垃圾回收。
这也是 ZGC 能实现极低停顿和高并发的核心原因之一。
在 ZGC(以及 G1)中,当一个 Region 里的存活对象都被搬迁到其他 Region 后,这个 Region 就会被标记为"空 Region" 。
下次需要分配内存时,JVM 可以直接把新的对象分配到这些空 Region,无需清理原有内容,直接覆盖即可。
这种机制大大提升了回收效率,避免了传统 GC 需要遍历和清理每个垃圾对象的低效操作。
ZGC 和 G1 对比
下面详细对比 ZGC 和 G1,并解释为什么 JDK 目前默认还是 G1。
一、ZGC 与 G1 的详细对比
特性 | G1 GC | ZGC |
---|---|---|
JDK版本 | JDK7u4 引入,JDK9 起默认 | JDK11 引入(实验),JDK15 起正式可用 |
目标 | 可预测低停顿,适合大堆和服务端 | 极低延迟(<10ms),适合超大堆和极致低延迟 |
堆结构 | Region 划分,逻辑新生代/老年代 | Region 划分,无固定新生代/老年代 |
回收算法 | 标记-整理(老年代),复制(新生代) | 标记-整理+并发搬迁,全阶段并发 |
STW 停顿 | 停顿可控(几十到几百 ms),与堆大小相关 | 停顿极短(<1~10ms),与堆大小几乎无关 |
并发能力 | 标记/清理并发,回收/整理需 STW | 几乎全并发,只有极短初始/搬迁 STW |
大对象处理 | Humongous Region,仍有碎片风险 | 大对象专用 Region,碎片极少 |
元空间回收 | Full GC 时回收 | Full GC 时回收 |
调优难度 | 参数较多,调优较灵活 | 参数极少,几乎无需调优 |
成熟度 | 非常成熟,广泛应用 | 新一代,JDK11+,部分特性还在完善 |
平台兼容性 | 仅支持 64 位,支持主流 64 位平台 | 仅支持 64 位,部分平台/容器不支持 |
默认 GC | JDK9+ 默认 | 需手动指定 -XX:+UseZGC |
二、ZGC 的优势
- 极低延迟:GC 停顿时间几乎恒定在 1~10ms,无论堆多大。
- 超大堆支持:TB 级堆无压力。
- 全并发:GC 各阶段几乎全并发,应用线程几乎不受影响。
- 调优简单:参数极少,开箱即用。
三、G1 的优势
- 成熟稳定:已成为 JDK 默认 GC,兼容性和稳定性极高。
- 可预测停顿 :可通过
-XX:MaxGCPauseMillis
设定目标停顿,适合大多数场景。 - 广泛兼容:支持所有主流 64 位平台和容器环境。
- 调优灵活:参数丰富,适合多种业务需求。
四、为什么 JDK 默认还是 G1?
-
成熟度和兼容性
- G1 已经在生产环境广泛验证多年,兼容性极高,适合绝大多数 Java 应用。
- ZGC 虽然优秀,但 JDK11 才引入,部分特性和平台支持还在完善,生态和工具链还不如 G1 成熟。
-
适用范围
- G1 适合大多数服务端、Web、微服务等场景,延迟和吞吐平衡好。
- ZGC 主要针对极致低延迟和超大堆场景(如金融、在线交易、实时分析等),普通应用用不上它的全部优势。
-
默认策略保守
- JDK 默认 GC 需兼顾稳定性、兼容性和广泛适用性,G1 是当前最合适的选择。
- ZGC 作为新技术,逐步推广和完善,未来有可能成为默认,但目前还不是。
-
平台与功能限制
- ZGC 只支持 64 位,部分平台/容器/老旧系统不支持。
- G1 支持更广泛的运行环境。
五、总结
- ZGC 更先进,适合极致低延迟和超大堆,但目前还不如 G1 成熟和通用。
- G1 兼容性、稳定性、适用范围更广,是 JDK 默认 GC 的最佳选择。
- 未来趋势:随着 ZGC、Shenandoah 等新一代 GC 的成熟,JDK 默认 GC 可能会发生变化,但目前 G1 依然是主流。