从地震应急系统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 秒后服务重启。 启动脚本是一个循环守护脚本:
: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 退出。
二、jstat 工具快速入门
排查 GC 问题最常用的工具是 jstat。一条命令即可看到堆内存的实时状态:
jstat -gcutil <PID> 2000 3
# 参数含义:每 2 秒输出一次,共 3 次
输出各列的含义:
| 列 | 全称 | 含义 | 关注优先级 |
|---|---|---|---|
| S0 / S1 | Survivor 0 / 1 使用率 | 两个 Survivor 空间已用百分比。ParallelGC 固定两块,G1 下此值显示不准确 | 低 |
| E | Eden 使用率 | 新生代 Eden 区已用百分比。上涨 → 对象在分配;下降 → 发生了 Young GC | 中 |
| O | Old 使用率 | 老年代已用百分比。上涨说明对象在晋升,持续高位可能触发 Full GC | 最高 |
| M | Metaspace 使用率 | 元空间(类信息)已用百分比。持续上涨说明类加载泄漏 | 低(只需关注趋势) |
| CCS | Compressed Class Space | 压缩类空间使用率 | 低 |
| YGC | Young GC 次数 | 新生代 GC 总次数 | 中 |
| YGCT | Young GC Time | 新生代 GC 累计耗时(秒) | 中 |
| FGC | Full GC 次数 | 老年代 GC 总次数。关注核心,>0 意味着 Stop-The-World 停顿时长 | 最高 |
| FGCT | Full GC Time | Full GC 累计耗时(秒) | 高 |
| GCT | GC Total Time | GC 总耗时 = YGCT + FGCT | 低(综合参考) |
关注优先级:O > FGC > YGC > E。O 和 FGC 决定了系统会不会卡顿甚至崩溃。
三、为什么渲染会超时?GC 问题
jstat 抓到的空闲时基线
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 的实测验证
修改启动脚本
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 主动回收 Old
O: 52.53% → 9.90% ← G1 Mixed GC 回收了上一轮滞留在 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)
五、两种垃圾收集器的内存曲线对比
实测得到的 jstat 数据,能直观看到两种 GC 的风格完全不同。
ParallelGC 风格——阶梯式
空闲时:
S0 S1 E O FGC
98.40 0.00 83.09 24.67 6
曲线形态:
O
↑
100% ─
│ ▄ Full GC 触发(停几秒)
│ ▄▄▄▄▄█
│ ▄▄█
│▄▄█
0% └──────────────────────→ 时间
特点:固定两块 Survivor(S0/S1),挑高一个另一个清零。Old 涨了就掉不下来,等满了触发 单线程 Full GC,暂停几秒到十几秒,O 骤降后再开始新一轮平缓上涨。
G1 风格——锯齿式
渲染高峰时:
S0 S1 E O FGC
0.00 100.00 85.97 52.53 0
0.00 100.00 8.73 9.90 0 ← 一次 GC 后 O 从 52% 跌到 9.9%
曲线形态:
O
↑
100% ─
│ ▄▄▄
│ ▄▄▄▄▄█ █▄▄▄ Mixed GC 后台回收
│▄▄█ █▄
│
0% └──────────────────────→ 时间
特点:S0/S1 在 G1 下无固定值。O 涨了能被 Mixed GC 后台回收降下来。全程 FGC=0,所有 GC 都是几十毫秒的 Young GC。
对比总结
| 维度 | ParallelGC | G1 |
|---|---|---|
| S0/S1 行为 | 严格两块固定大小,一次用一边 | jstat 显示不准,G1 用多个 Region 动态分配 |
| O 曲线 | 涨了就掉不下来,等满了触发 Full GC 骤降 | 涨了能自动降(Mixed GC 后台回收),曲线更平缓 |
| FGC | 累积增加,每次几秒停顿 | 始终 0,或极少出现 |
| 整体形态 | 阶梯式——平稳→涨满→卡死→骤降→再平稳 | 锯齿式——涨→Young GC→回收→再涨→混合回收→降 |
| 本质差异 | 等到 Old 满了才一股脑回收(停顿大) | 在后台分批偷偷回收(停顿小) |
六、经验总结
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. 守护脚本本身不是问题,但掩盖了问题
循环守护脚本让服务看起来"自己恢复了",实际上每次重启都丢失了正在处理的专题图和简报任务。应该从应用层防止崩溃,而不是依赖脚本兜底。
6. 关注优先级:O > FGC > YGC > E
排查 GC 问题时,不要被 S0/S1 的数值波动干扰(尤其在 G1 下),也不要只看 YGC 次数。O(Old 使用率)和 FGC(Full GC 次数) 决定了系统会不会卡顿甚至崩溃。优先看这两列,其他数据作为辅助参考。
附录:常用排查命令
# 查看 Java 进程
jps -l | findstr earthquake
# GC 状态监控(2 秒一次,3 次)
jstat -gcutil <PID> 2000 3
# 查看 JVM 参数是否生效
jcmd <PID> VM.flags | findstr "G1 GC"
# GC 日志分析
jstat -gccause <PID> 2000
浙公网安备 33010602011771号