
每天定时出现的几秒级的STW停顿曾经是高性能Java应用的一个顽疾。当CMS在老年代碎片化、并发模式失效以及晋升失败的情况下被强制降级为Serial Old全堆回收时,就会出现接口毛刺和超时告警等问题。
切换到G1并不是简单地把-XX:+UseConcMarkSweepGC换成-XX:+UseG1GC那么简单。G1按照区域来增量回收,首先清除掉垃圾最多的那个区域,在机制上减少了碎片产生的可能性,用可以预测的停顿代替了CMS“并发收集+兜底Full GC”的不确定性方式。
新的关键参数位置
- 堆固定:
-Xms与-Xmx对等,杜绝运行时伸缩带来的额外停顿; - 停顿目标:
-XX:MaxGCPauseMillis=150~200,设为软目标而非硬上限,过低会挤压年轻代、抬高Young GC频次; - **提前设置好
-XX**:InitiatingHeapOccupancyPercent=35~45参数,使混合垃圾回收器在老年代压力到来之前就进行工作。 - 大对象治理:
-XX:G1HeapRegionSize=16m配合流式处理,压住Humongous对象分配; - 预留空间:
-XX:G1ReservePercent=10,缓解Evacuation Failure; - 元空间兜底:
-XX:MetaspaceSize与-XX:MaxMetaspaceSize显式封顶,避免类加载膨胀诱发Full GC; - 日志可观测:
-Xlog:gc*=info配合堆转储开关,留足复盘依据。
真实的收敛效果
某订单服务迁移之后,Full GC从原来的每天多次、每次1.8秒左右变成了连续几天都没有出现,P99延迟毛刺也消失了,Young GC平均值被控制在50ms以内。剩下的偶发长时间停顿主要是大对象批量处理和本地缓存无限增长造成的——不能靠参数来阻止,只能顺便进行分块查询以及缓存驱逐策略收尾。
总结
从CMS到G1的转变的意义不在于改名,而是把不能控制的全部STW分解为可以调度、可以观察和可以衰减的部分回收。参数只是放大器,真正的稳定来自于“收集器机制+内存分配模型+代码侧大对象治理”这三者的统一。