SheepDog1998

博客园 首页 新随笔 联系 订阅 管理

从地震应急系统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
posted on 2026-07-29 17:45  SheepDog1998  阅读(8)  评论(0)    收藏  举报