硬核实战:G1 与 ZGC 在支付系统中的选型与调优

5 阅读4分钟

硬核实战:G1 与 ZGC 在支付系统中的选型与调优

前置说明:本文基于 OpenJDK 21,聚焦生产级 GC 调优经验。不是参数罗列表,而是告诉你「什么场景选什么 GC、怎么调、怎么验证」。


一、为什么支付系统必须重视 GC

支付系统有两个天然 GC 压力源:

1. 大量短生命周期对象

  • 每笔支付创建 PayRequestPayResponseTransactionLogChannelCallback
  • 高峰期 TPS 3000+,每秒创建数十万对象
  • 这些对象在 Minor GC 时必须回收干净

2. 大堆敏感

  • 支付系统堆通常 8~16GB,GC 停顿直接影响接口 P99
  • 用户感知到的「支付慢」,90% 是 GC 停顿,不是业务逻辑
GC 停顿对支付的影响链:
GC STW 200ms
    → 线程阻塞,Tomcat 线程池打满
    → 新请求排队
    → 支付接口 P99 飙到 500ms+
    → 前端超时展示"支付中"
    → 用户重复点击
    → 幂等压力翻倍
    → 系统雪崩

二、G1 vs ZGC:核心区别

维度G1 GCZGC
停顿模型可预测停顿(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% → 告警

关联阅读