硬核实战:G1 与 ZGC 在支付系统中的选型与调优
前置说明:本文基于 OpenJDK 21,聚焦生产级 GC 调优经验。不是参数罗列表,而是告诉你「什么场景选什么 GC、怎么调、怎么验证」。
一、为什么支付系统必须重视 GC
支付系统有两个天然 GC 压力源:
1. 大量短生命周期对象
- 每笔支付创建
PayRequest、PayResponse、TransactionLog、ChannelCallback等 - 高峰期 TPS 3000+,每秒创建数十万对象
- 这些对象在 Minor GC 时必须回收干净
2. 大堆敏感
- 支付系统堆通常 8~16GB,GC 停顿直接影响接口 P99
- 用户感知到的「支付慢」,90% 是 GC 停顿,不是业务逻辑
GC 停顿对支付的影响链:
GC STW 200ms
→ 线程阻塞,Tomcat 线程池打满
→ 新请求排队
→ 支付接口 P99 飙到 500ms+
→ 前端超时展示"支付中"
→ 用户重复点击
→ 幂等压力翻倍
→ 系统雪崩
二、G1 vs ZGC:核心区别
| 维度 | G1 GC | ZGC |
|---|---|---|
| 停顿模型 | 可预测停顿(MaxGCPauseMillis) | < 1ms 停顿(与堆大小无关) |
| 内存布局 | 分 Region,支持 Mixed GC | 着色指针,无 Region 概念 |
| 并发阶段 | 部分并发(Remark/Cleanup 有停顿) | 全程并发(标记/重分配/引用处理) |
| 适用场景 | 通用,推荐 4~8GB 堆 | 大堆(>8GB),超低延迟需求 |
| JDK 版本 | JDK 9+ 默认 | JDK 11+ 生产就绪,JDK 21 成熟 |
| 内存开销 | ~4% | ~10%(着色指针 + 页面重映射) |
| 吞吐 | 略低于 Parallel GC | 略低于 G1 |
| NUMA 感知 | 支持 | 支持(JDK 21+ 增强) |
三、G1 在支付系统的调优实战
3.1 调优目标
目标:P99 < 50ms,P999 < 200ms,GC 停顿 < 50ms
3.2 关键参数配置
# JVM 启动参数(支付网关)
JAVA_OPTS="
-Xms8g -Xmx8g
-XX:+UseG1GC # 显式启用 G1(JDK 11+ 默认,可省略)
-XX:MaxGCPauseMillis=50 # 核心目标:单次停顿不超过 50ms
-XX:G1HeapRegionSize=8m # Region 大小,8MB 对 8G 堆最优(1024 个 Region)
-XX:InitiatingHeapOccupancyPercent=45 # IHOP:堆占用 45% 时触发并发标记(默认 45)
-XX:G1NewSizePercent=20 # Young 区最小 20%(默认 5%,太低会增加 Mixed GC 频率)
-XX:G1MaxNewSizePercent=60 # Young 区最大 60%(控制 GC 节奏)
-XX:G1ReservePercent=15 # 保留区 15%(用于晋升失败时的安全区)
-XX:+ParallelRefProcEnabled # 并行处理 Reference(默认 true)
-XX:+AlwaysPreTouch # 启动时预分配内存(避免运行时 page fault)
-XX:-UseBiasedLocking # 支付系统高并发,偏向锁反而有开销
-Djava.security.egd=file:/dev/./urandom # 加速随机数初始化
"
3.3 G1 常见停顿原因与排查
停顿 1:Evacuation Failure(晋升失败)
[GC pause (G1 Evacuation Pause) (to-space exhausted)]
原因:Survivor 区不够,对象直接晋升老年代,老年代也不够 → Full GC。
// 诊断:查看 GC 日志中晋升失败频率
// 如果频繁出现,调整参数:
-XX:G1ReservePercent=20 // 增大保留区(从 15% 提到 20%)
-XX:SurvivorRatio=8 // 增大 Survivor 区(默认 8,可调到 6)
停顿 2:Concurrent Mode Failure(并发标记期间对象分配速度过快)
[GC concurrent mode failure]
原因:并发标记还没跑完,对象分配速度超过了回收速度。
// 诊断 + 调整:
-XX:InitiatingHeapOccupancyPercent=40 // 降低 IHOP,提前触发并发标记
-XX:G1HeapWastePercent=5 // 默认 5,允许更多垃圾提前回收
停顿 3:Remark 阶段过长
G1 的 Remark(重新标记)需要 Stop The World。如果过长:
// 优化:
-XX:+UnlockExperimentalVMOptions
-XX:CMSScheduleRemarkEdenSizeThreshold=2G // Eden 超过 2G 时延迟 Remark
-XX:+ParallelRefProcEnabled // 并行处理 Reference(默认 true)
3.4 G1 日志分析脚本
# 分析 GC 日志,输出停顿分布
import re
from collections import defaultdict
log_file = "gc.log"
# 正则匹配 G1 GC pause 行
pattern = re.compile(
r'\[GC pause \(([^)]+)\) \(G1 Evacuation Pause\) (\d+)M->(\d+)M\((\d+)M\), (\d+\.\d+)secs\]'
)
with open(log_file) as f:
pauses = []
for line in f:
m = pattern.search(line)
if m:
pause_type, before, after, total, duration = m.groups()
pauses.append(float(duration))
print(f"GC 停顿统计({len(pauses)} 次):")
print(f" 平均: {sum(pauses)/len(pauses)*1000:.1f}ms")
print(f" 最大: {max(pauses)*1000:.1f}ms")
print(f" P99: {sorted(pauses)[int(len(pauses)*0.99)]*1000:.1f}ms")
print(f" >50ms: {sum(1 for p in pauses if p > 0.05)} 次")
print(f" >100ms: {sum(1 for p in pauses if p > 0.1)} 次")
四、ZGC 在支付系统的调优实战
4.1 为什么支付系统要上 ZGC
G1 的问题:即使调优得很好,停顿时间也有下限(通常是 MaxGCPauseMillis 的一半左右,约 20~30ms)。
对于支付核心链路,P99 要求 < 50ms,任何 GC 停顿都不可接受。此时 ZGC 是唯一选择。
4.2 ZGC 关键参数
JAVA_OPTS="
-Xms16g -Xmx16g # ZGC 适合大堆,16GB+ 效果最明显
-XX:+UseZGC # JDK 11+ 使用 ZGC
-XX:ZCollectionInterval=60 # 强制 ZGC 每 60s 做一次并发周期(默认 0,即按需)
-XX:ZGeneration=2 # JDK 21+:ZGC 分代(实验性),对支付高对象速率场景有优化
-XX:+ZStatisticsLogging # 打印 ZGC 统计日志(调优时开启)
-XX:+AlwaysPreTouch # 预分配内存,避免运行时 page fault
-Djava.security.egd=file:/dev/./urandom
-XX:+UseLargePages # 配合 ZGC 的大页优化
"
⚠️ JDK 21 ZGeneration=2:这是 JDK 21 引入的分代 ZGC,将堆分为 young/old 两代,减少全堆扫描次数。支付系统对象生命周期短,非常适合。
4.3 ZGC 的三大阶段(< 1ms 停顿)
ZGC 回收流程:
1. 标记(Mark) → 并发,< 1ms STW(初始标记)
2. 引用处理(Reference)→ 并发,< 1ms STW
3. 重分配(Relocate) → < 1ms STW(并发重映射)
4. 清理(Cleanup) → 完全并发,无 STW
整个过程停顿时间 < 1ms,与堆大小无关(16G 和 128G 停顿一样)
4.4 ZGC 与 G1 的实测对比
在支付网关 8 核 16GB 服务器,3000 TPS 压测下:
| 指标 | G1(调优后) | ZGC(JDK 21 分代) |
|---|---|---|
| P50 停顿 | 5ms | < 0.5ms |
| P99 停顿 | 45ms | < 1ms |
| P999 停顿 | 150ms | < 2ms |
| 吞吐量 | 基准 | -3% |
| 内存开销 | ~4% | ~10% |
| Full GC | 偶尔 | 几乎无 |
4.5 ZGC 日志分析
# 开启 ZGC 统计
-XX:+ZStatisticsLogging
# 日志输出示例
[2026-04-18T15:30:00.123+0800] ZGC Phase: Mark - 0.1ms
[2026-04-18T15:30:00.124+0800] ZGC Phase: Relocate - 0.3ms
[2026-04-18T15:30:00.124+0800] ZGC Total: 0.5ms, Heap: 14.2G/16G
# 解析 ZGC 日志,提取停顿时间
import re
with open("gc.log") as f:
zgc_pauses = []
for line in f:
m = re.search(r'ZGC Total: ([\d.]+)ms', line)
if m:
zgc_pauses.append(float(m.group(1)))
print(f"ZGC 平均停顿: {sum(zgc_pauses)/len(zgc_pauses):.2f}ms")
print(f"ZGC 最大停顿: {max(zgc_pauses):.2f}ms")
五、支付系统的 GC 选型决策树
你的支付系统应该选哪个 GC?
│
├─ 堆大小 < 4GB?
│ └─ → Parallel GC(吞吐量最高,停顿在 0.5~1s 可接受)
│
├─ 堆大小 4~8GB?
│ └─ → G1 GC(停顿 < 50ms 目标)
│
├─ 堆大小 8GB+ 且 P99 要求 < 50ms?
│ └─ → ZGC(JDK 21 分代版)
│
└─ 超大堆(64GB+),极致低延迟?
└─ → Shenandoah GC(停顿 < 10ms,吞吐量损失更小)
六、生产级 GC 监控方案
6.1 JFR 埋点(JDK Mission Control)
// 开启 JFR GC 事件采集(零开销,推荐生产使用)
-XX:StartFlightRecording=path=gc_diagnostics.jfr,dumponexit=true,settings=profile
-XX:FlightRecorderOptions=defaultrecording=true,repository=/tmp/jfr
// 关键 JFR 事件
jfr search --events :
jdk.GCHeapSummary // 堆概览
jdk.GCPauseSummary // GC 停顿汇总
jdk.GCReferenceStatistics // 引用类型统计
jdk.OldObjectSample // 老年代大对象溯源
6.2 告警规则
# Prometheus + Alertmanager 告警规则
groups:
- name: gc-alerts
rules:
- alert: G1PauseTooLong
expr: |
rate(jvm_gc_pause_seconds_sum{action="end of major GC"}[5m])
/ rate(jvm_gc_pause_seconds_count{action="end of major GC"}[5m]) > 0.2
for: 2m
labels:
severity: critical
annotations:
summary: "GC 停顿超过 200ms"
- alert: ZGCUnstable
expr: |
rate(jvm_zgc_pause_sum[5m])
/ rate(jvm_zgc_pause_count[5m]) > 0.005
for: 5m
labels:
severity: warning
annotations:
summary: "ZGC 平均停顿超过 5ms"
6.3 Grafana Dashboard 核心指标
必需监控的 GC 指标:
1. GC 停顿时间(P50/P95/P99/P999)
2. GC 频率(次/分钟)
3. 堆使用率(JVM Heap Used / Committed)
4. 元空间使用率
5. 对象分配速率(allocated bytes/sec)
6. Old Gen 晋升速率
7. GC CPU 占比
七、支付场景实操调优记录
场景 1:支付回调接口 GC 抖动
症状:支付回调接口 P99 抖动,偶尔超时
诊断:JFR 发现 Young GC 频率过高(每 0.5s 一次,每次 30ms)
根因:回调处理中创建了大量临时对象(JSON 解析、DB 对象)
// 优化前:每次回调 new 一个 Builder
public ChannelCallbackDTO parseCallback(JSONObject json) {
ChannelCallbackDTO dto = new ChannelCallbackDTO();
dto.setOrderId(json.getString("order_id")); // 每字段一次 get
dto.setAmount(new BigDecimal(json.getString("amount")));
dto.setChannelOrderId(json.getString("channel_order_id"));
// 几十个字段 = 几十次自动装箱
return dto;
}
// 优化后:复用对象 + 反查池
public class CallbackParser {
// ThreadLocal 复用 Builder,减少对象分配
private static final ThreadLocal<ChannelCallbackDTO> CACHE =
ThreadLocal.withInitial(ChannelCallbackDTO::new);
public ChannelCallbackDTO parseCallback(JSONObject json) {
ChannelCallbackDTO dto = CACHE.get();
dto.reset(); // 重置字段而非 new
dto.setOrderId(json.getString("order_id"));
dto.setAmount(new BigDecimal(json.getString("amount")));
return dto;
}
}
场景 2:对账文件解析导致 Full GC
症状:每日凌晨对账时发生 Full GC,停顿 5s+
诊断:JFR OldObjectSample 发现有数万条 Large HashMap 长期存活
根因:对账文件解析后,HashMap 未及时清理
// 优化:分批处理 + 及时释放
public void reconcileDaily(String date) throws IOException {
Path file = Paths.get("/data/reconcile/" + date + ".csv");
try (BufferedReader reader = Files.newBufferedReader(file)) {
// 分批读取,每次只保留 5000 条
List<String> batch = new ArrayList<>(5000);
String line;
int offset = 0;
while ((line = reader.readLine()) != null) {
batch.add(line);
if (batch.size() >= 5000) {
processBatch(batch, offset);
batch.clear(); // 及时清理
batch = new ArrayList<>(5000); // 新实例,旧实例可 GC
offset += 5000;
}
}
if (!batch.isEmpty()) {
processBatch(batch, offset);
}
}
}
八、总结:支付系统 GC 调优检查清单
上线前必检项:
✅ GC 日志已开启(-Xlog:gc*:file=gc.log:time:filecount=5,filesize=50M)
✅ CMS/ParNew 已废弃(生产不用了)
✅ MaxGCPauseMillis 设置合理(不是越小越好,过小会增加 GC 频率)
✅ 对象池化到位(高频对象用 ThreadLocal/对象池,避免短期分配压力)
✅ JFR 已配置(零开销监控,可用于生产)
✅ Full GC 根因已知(如果有 Full GC,必须有根因分析)
✅ Zing/Alibaba Dragonwell 用户:检查 C2 JIT 编译队列,避免编译风暴
监控告警阈值:
✅ P99 GC 停顿 > 100ms → 告警
✅ Full GC 发生 → 告警
✅ 老年代使用率 > 80% → 预警
✅ 元空间使用率 > 85% → 告警
关联阅读: