Java 25的ZGC通过并发垃圾回收大幅降低尾延迟,实测显示其在微服务中表现优于G1,尤其p999和p9999延迟更低。但高CPU负载下需注意分配停顿,适合资源充足场景。
ZGC(Z Garbage Collector)!它可是Java 25的明星功能,号称能大幅降低应用的尾延迟,让你的微服务跑得更快、更稳!
谁是马林?技术圈的硬核探秘家!
先来介绍一下文章的作者——马林(Gunnar Morling)。他是一位在数据流和Java生态圈深耕多年的技术大咖,专注于开源项目、Kafka、Flink等前沿技术。他的博客“morling.dev”以深入浅出的风格,剖析各种技术趋势和工具,深受开发者喜爱。这次,他把目光投向了Java 25的新特性——ZGC,带我们一探究竟!
ZGC是什么?为什么它这么火?
ZGC,全称Z Garbage Collector,是Java 11引入的一种并发垃圾回收器,到了Java 25,它升级成了“代际ZGC”,成为唯一版本,并且首次获得长期支持(LTS)。简单来说,ZGC的目标是消灭那些让Java程序员头疼的垃圾回收(GC)暂停,降低应用的尾延迟(p99、p999、p9999等高百分位延迟)。
尾延迟是什么?就是那些偶尔出现的超长响应时间,可能会让你的用户抓狂!
传统的垃圾回收器,比如Java 9以来的默认选择G1,会在回收内存时暂停应用线程,导致响应时间波动。而ZGC的杀手锏在于,它把大部分垃圾回收工作交给独立的GC线程,应用线程几乎不受影响,GC暂停时间被压缩到亚毫秒级(不到1毫秒)!这意味着什么?你的微服务、Web应用,甚至大数据处理系统,都能拥有更平滑的响应时间,用户的体验会更丝滑!
当然,天下没有免费的午餐。ZGC虽然降低了延迟,但它需要额外的CPU资源来运行并发GC线程,这可能会牺牲一些系统吞吐量。马林在文章中特别提到,他对ZGC的默认设置表现更感兴趣,因为现实中,大多数开发者并不会花时间去调优GC参数。毕竟,谁有空天天盯着JVM参数调整啊?工作负载一变、Java版本一更新,之前的优化可能就白费了。所以,ZGC的开箱即用性能,才是真正的亮点!
实测对比:ZGC vs G1,谁更胜一筹?
为了验证ZGC的实力,马林用一个基于Quarkus框架的微服务进行了测试。这个服务从Postgres数据库返回数据,模拟了1000请求/秒的负载,运行在Hetzner CCX43实例上(4核CPU,4GB内存)。测试持续两分钟,排除前30秒的预热影响,结果让人眼前一亮!
- 延迟分布:在p99(99%请求的延迟)以下,ZGC和G1的表现几乎一样,说明日常场景下两者差别不大。但到了p999(99.9%)和p9999(99.99%)的高百分位延迟,ZGC完胜!G1的尾延迟明显更高,偶尔会出现20毫秒以上的GC暂停,而ZGC的暂停时间仅为50微秒,简直是“无感”级别!
- 可视化结果:马林用Vegeta负载生成器生成了请求延迟图,G1的图表显示出明显的延迟“尖刺”,而ZGC的曲线平滑如丝,几乎没有异常波动。
- JFR验证:通过JDK Flight Recorder(JFR)分析,G1的GC暂停时间确实是尾延迟的罪魁祸首,而ZGC的超短暂停时间让它在高负载场景下表现更稳定。
这个测试的结论是:仅靠切换到ZGC,不用任何调优,你就能显著降低尾延迟!
这对追求极致性能的微服务开发者来说,简直是福音。当然,马林也提醒大家,G1通过设置参数(比如-XX:MaxGCPauseMillis
)也能优化,但ZGC的默认表现已经足够惊艳。
ZGC的“软肋”:分配停顿的隐藏危机
ZGC听起来完美无缺,但它也有自己的“小心思”。马林设计了一个更极端的测试场景:一个合成基准测试,模拟高强度的对象分配(每秒12GB和30GB)。结果如何?
- 12GB/秒分配:ZGC依然表现优异,p999和p9999延迟远低于G1,证明它在中等压力下依然稳如老狗。
- 30GB/秒分配:情况反转!当系统满载运行(用尽16核CPU),G1的延迟反而比ZGC低。为什么?因为ZGC的并发GC线程需要更多CPU资源,在CPU资源紧张时,ZGC来不及清理内存,导致“分配停顿”(ZAllocationStall)。
分配停顿是什么?
简单说,当ZGC的GC线程跟不上应用分配对象的速度时,它会暂停分配新对象,直到内存被清理出来。
这种停顿虽然不像G1的GC暂停那样直接影响应用线程,但也会增加尾延迟。马林通过JFR发现了这些停顿,并建议开发者通过监控ZAllocationStall事件,及时发现问题。如果你的应用经常遇到这种情况,可能需要用性能分析器找出瓶颈,或者直接加点CPU资源!
代际ZGC:Java 25的秘密武器
Java 25的ZGC升级成了“代际ZGC”,基于“弱代际假设”:大部分对象在创建后很快变成垃圾。代际ZGC将堆分为“新生代”和“老年代”,新生代的对象被频繁扫描清理,而老年代的扫描频率较低,从而更高效地利用CPU资源。相比Java 17的单代ZGC,代际ZGC在吞吐量和尾延迟上都有显著提升。
马林指出,Java 25是首个默认支持代际ZGC的LTS版本(Java 21也支持,但需要手动启用)。
这意味着,现在是尝试ZGC的最佳时机!只需要在启动JVM时加上-XX:+UseZGC
,你就能体验到它的威力。
ZGC适合你吗?因地制宜最重要!
ZGC虽然强大,但并非万能。马林强调,垃圾回收器的选择取决于你的具体工作负载和运行环境。如果你的应用CPU负载较高,ZGC的并发线程可能会“抢”资源,导致性能下降。这种情况下,G1可能更适合。反之,如果你的系统有足够的CPU余量,ZGC的低延迟特性会让你的应用如虎添翼。
对于分布式系统,ZGC的吞吐量损失可以通过横向扩展(加节点)来弥补。马林建议大家在自己的环境中测试ZGC,方法很简单:加上-XX:+UseZGC
运行一下,看看效果如何!他还分享了测试代码的链接,感兴趣的小伙伴可以自己动手试试。
总结:ZGC,Java性能优化的新选择!
ZGC是Java 25的一大亮点,通过并发垃圾回收,它几乎消除了GC暂停,让尾延迟低到“飞起”!无论是微服务还是大数据处理,ZGC都能带来显著的性能提升,尤其在默认设置下表现尤为出色。但别忘了,它对CPU资源的需求更高,适合CPU余量充足的场景。快去试试-XX:+UseZGC
,让你的Java应用跑得更稳更快!