从地震应急系统GC调优看JVM参数选型实战

从地震应急系统GC调优看JVM参数选型实战

背景

某省级地震应急响应系统,核心业务是接入地震台网数据后自动生成评估报告和专题图(SuperMap 渲染)。系统异常重启、专题图缺失等问题影响出图稳定性。


一、专题图缺失的排查

现象

青海兴海3.7级地震产出的专题图数量不够,但重新模拟一次数量就正常了------这是一个偶发问题。

最终根因:日志

从 51MB 的日志文件中找到关键错误:

复制代码
2026-07-28 23:59:07.799 [EqTask-3] ERROR
    专题图生成超时,专题图:Earthquake_epicenter
    java.util.concurrent.TimeoutException: null
     ... CompletableFuture.timedGet(...)
     ... ThematicMapServiceImpl.java:325

2026-07-28 23:59:07.799 [EqTask-4] ERROR
    专题图生成超时,专题图:EQ_Incidence

2026-07-28 23:59:07.799 [EqTask-1] ERROR
    专题图生成超时,专题图:EQ_History_1950

2026-07-28 23:59:33.963 [main] INFO
    Starting EarthquakeEmergencyGS... PID 297320  ← 服务已重启

3 张图同时超时,26 秒后服务重启。 启动脚本是一个循环守护脚本:

batch 复制代码
:RUN
if not defined FIRST_RUN (
    set FIRST_RUN=1
) else (
    echo 服务异常退出,即将自动重启
    timeout /t 5 /nobreak >nul
)
java ... -jar earthquakeEemergencyGS-1.0-SNAPSHOT.jar
goto RUN

触发链

复制代码
SuperMap 渲染超过 5 分钟
  → future.get(5, TimeUnit.MINUTES) 超时
    → future.cancel(true)  // 向渲染线程发送 interrupt()
      → SuperMap JNI 层 C++ 代码被中断,JVM 崩溃
        → 守护脚本检测到 java.exe 退出
          → "服务异常退出,即将自动重启"

关键问题cancel(true) 会调用 Thread.interrupt()。SuperMap 是 JNI 调 C++ 驱动的,Java 层的中断传递到 C++ 层时可能导致段错误或未处理异常,直接让 JVM 退出。


二、为什么渲染会超时?GC 问题

jstat 抓到的空闲时基线

bash 复制代码
jstat -gcutil <PID> 2000 3
复制代码
  S0     S1     E      O      M     YGC    YGCT    FGC    FGCT
 98.40   0.00  83.09  24.67  93.38    16    1.314     6    2.514

空闲状态下的关键指标:

指标 含义
S0 98.40% Survivor 空间几乎已满
O 24.67% 空闲时 Old Gen 就占了四分之一
FGC 6 已经发生过多次 Full GC(即使排除手动触发)

根因分析

Java 8 默认使用 ParallelGC,默认 SurvivorRatio=8

复制代码
Eden : Survivor1 : Survivor2 = 8 : 1 : 1
Survivor ≈ 堆的 1%

SuperMap 渲染 A3 专题图时生成大量临时对象(地图数据集、几何对象、位图缓存),这些对象在渲染期间一直存活,不会被第一轮 Young GC 回收掉,直接晋升到 Old Gen。Old Gen 满了就触发 Full GC。

ParallelGC 的 Full GC 是单线程的 ,4G 堆扫描一次几秒到十几秒。渲染线程被暂停 → 出图变慢 → 超时窗口被吃掉 → 超时 → cancel(true) → JVM 崩溃。


三、切换 G1GC 的实测验证

修改启动脚本

batch 复制代码
java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
     -Xms4g -Xmx4g
     -XX:SurvivorRatio=4
     -XX:+UseG1GC
     -jar earthquakeEemergencyGS-1.0-SNAPSHOT.jar

参数说明

参数 作用
-Xms4g -Xmx4g 堆固定 4G,减少动态扩缩带来的 GC 波动
-XX:SurvivorRatio=4 Survival 从 1% 扩大到 2%(与 G1 配合,给 Young Region 更多余量)
-XX:+UseG1GC 切换到 G1,Full GC 多线程、有后台并发回收

G1 实测数据(大震级渲染全程)

复制代码
Phase 1: 渲染前基线
O=47.46%, YGC=16, FGC=0

Phase 2: Young GC 触发,G1 Mixed GC 回收 Old
O: 52.53% → 9.90%    ← G1 主动回收了上一轮滞留在 Old 的对象
YGC: 16 → 17

Phase 3: 渲染高峰期,连续 Young GC
YGC: 17 → 22 → 26 → 28
YGCT: 1.634 → 1.703 → 1.991 → 2.012 → 2.100 → 2.129
每次 Young GC 平均 ~41ms,渲染线程基本无感

Phase 4: 渲染结束
O=39.25%(稳定,不再增长)
FGC=0(全程零 Full GC)

与 ParallelGC 的对比

ParallelGC G1GC
Full GC 次数 多次(含手动) 0
Full GC 暂停 单线程,几秒~十几秒 不触发,或被 Mixed GC 替代
Young GC 平均暂停 几十毫秒 ~41ms
Old Gen 管理 满了就 Full GC 后台并发回收,O 从 52% 回落到 39%
JVM 稳定性 cancel(true) 导致崩溃 稳定
专题图产出 超时缺失 全部完成

四、经验总结

1. ParallelGC ≠ 错误,只是默认配置不够

ParallelGC 本身是优秀的吞吐型收集器,问题在于默认的 SurvivorRatio=8 不适合大对象突发场景 。如果当时不换 G1,调 -XX:SurvivorRatio=2 也能缓解,但无法解决 Full GC 单线程停顿的本质问题。

2. G1 不是不涨 O,是涨了还能回收

第一次切换后 O 涨到 99.97% 确实让人紧张,但第二次大震级测试中 G1 触发了 Mixed GC,O 从 52% 降回 9.9%,最终稳定在 39%。这是 G1 与 ParallelGC 最核心的差异------G1 允许 Old 几乎占满,因为它有后台并发回收撑着。ParallelGC 下 Old 满了就是 Full GC,停顿几秒起步。

3. Java 8 上 G1 已经足够成熟

Java 8 从 u40 开始 G1 就是 GA 状态。4G~6G 堆下,唯一代价是多 ~200MB 内存和略高一点的 CPU 开销,对 16 核服务器可以忽略。

4. cancel(true) + JNI 是不安全的组合

Thread.interrupt() 传递到 JNI 层时行为不可预测。专题图生成超时后的正确做法是 cancel(false) 不做中断,或者延长超时时间从根源上避免超时触发。

5. 守护脚本本身不是问题,但掩盖了问题

循环守护脚本让服务看起来"自己恢复了",实际上每次重启都丢失了正在处理的专题图和简报任务。应该从应用层防止崩溃,而不是依赖脚本兜底。


附录:常用排查命令

bash 复制代码
# 查看 Java 进程
jps -l | findstr earthquake

# GC 状态监控(2 秒一次,3 次)
jstat -gcutil <PID> 2000 3

# 查看 JVM 参数是否生效
jcmd <PID> VM.flags | findstr "G1 GC"

# GC 日志分析(需要启动时加 -Xloggc)
jstat -gccause <PID> 2000