从地震应急系统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