三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

JVM调优实战——从Full GC到零停顿的优化之路

JVM调优实战——从Full GC到零停顿的优化之路

JVM调优实战——从Full GC到零停顿的优化之路

问题来了

系统开始"咳嗽"。

不是宕机,不是报错,是每隔几小时就"卡"一下。前端响应从200ms跳到2000ms,持续30秒后恢复。

监控显示:Full GC

症状分析

1. 业务影响

  • 查询响应时间波动:200ms → 2000ms → 200ms
  • 交易成功率下降:99.9% → 99.5%
  • 用户投诉:“系统一会儿快一会儿慢”

2. 技术指标

  • JVM堆内存:8G
  • Young GC频率:每分钟2-3次
  • Full GC频率:每3-4小时1次
  • Full GC时长:20-30秒

3. 业务场景

  • 日均交易:50万笔
  • 并发用户:1000+
  • 数据量:人3000万,历史记录100亿+
  • 业务特点:早9点、下午2点高峰,月末结算峰值

诊断过程

第一步:收集证据

# 1. 查看GC原因jstat-gccause<pid>100010# 输出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC0.0096.8818.2372.4595.1292.3115632345.123451125.4561470.579Allocation Failure System.gc()

关键信息

  • LGCC(上次GC原因):Allocation Failure(分配失败)
  • GCC(当前GC原因):System.gc()(有人调了System.gc())
  • FGC:45次Full GC
  • FGCT:1125.456秒(累计Full GC时间)

第二步:分析堆内存

# 2. 查看堆内存分布jmap-heap<pid># 输出关键信息Heap Configuration: MinHeapFreeRatio=40MaxHeapFreeRatio=70MaxHeapSize=8589934592(8192.0MB)NewSize=268435456(256.0MB)MaxNewSize=2863661056(2731.0MB)OldSize=5439488(5.1875MB)NewRatio=2SurvivorRatio=8Heap Usage: PS Young Generation Eden Space: capacity=2147483648(2048.0MB)used=1234567890(1177.6MB)free=912919758(870.4MB)57.5% used From Space: capacity=268435456(256.0MB)used=0(0.0MB)free=268435456(256.0MB)0.0% used To Space: capacity=268435456(256.0MB)used=0(0.0MB)free=268435456(256.0MB)0.0% used PS Old Generation capacity=6442450944(6144.0MB)used=4567890123(4356.8MB)free=1874560821(1787.2MB)70.9% used

发现问题

  1. Old区使用率70.9%,接近触发Full GC的阈值
  2. Young区Eden 57.5%,Survivor区0%——对象直接进入Old区
  3. NewRatio=2(Young:Old=1:2),Young区偏小

第三步:查看GC日志

# 3. 开启GC日志java-XX:+PrintGCDetails-XX:+PrintGCDateStamps-XX:+PrintGCTimeStamps-Xloggc:gc.log...

GC日志分析:

34567.890: [GC (Allocation Failure) [PSYoungGen: 1177M->256M(2048M)] 4356M->3456M(8192M), 0.456 secs] 45678.901: [Full GC (System.gc()) [PSYoungGen: 256M->0M(2048M)] [ParOldGen: 3200M->2987M(6144M)] 3456M->2987M(8192M), 23.456 secs]

关键点

  1. Young GC后,存活对象256M,全部进入Old区
  2. Full GC由System.gc()触发,不是内存不足
  3. Full GC耗时23.456秒,停顿时间太长

根本原因

1. 代码问题:有人调了System.gc()

// 罪魁祸首:某个第三方库publicclassBadLibrary{publicvoidcleanup(){// 错误:手动触发Full GCSystem.gc();}}

2. 配置问题:Young区太小

  • NewRatio=2(Young:Old=1:2)
  • 实际业务:大量短期对象,应该给更大Young区
  • Survivor区使用率0%,说明对象年龄增长过快

3. 业务问题:大对象直接进入Old区

业务特点:

  • 查询结果集大(人列表)
  • 报表数据缓存
  • 批量处理中间结果

解决方案

第一步:禁止System.gc()

# JVM参数-XX:+DisableExplicitGC

效果:禁止代码中的System.gc()调用,Full GC频率立即下降。

第二步:调整堆内存比例

# 调整前-Xmx8g-Xms8g-XX:NewRatio=2# 调整后-Xmx8g-Xms8g-XX:NewRatio=1-XX:SurvivorRatio=6

参数解释

  • NewRatio=1:Young区:Old区=1:1(各4G)
  • SurvivorRatio=6:Eden:Survivor=6:1:1(Eden 3G,每个Survivor 0.5G)

第三步:优化GC策略

# 使用G1GC替代ParallelGC-XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:G1HeapRegionSize=4m-XX:InitiatingHeapOccupancyPercent=45

G1GC优势

  • 可预测的停顿时间(设置200ms目标)
  • 并发标记,减少STW时间
  • 自动区域划分,避免内存碎片

第四步:监控大对象

# 打印大对象分配堆栈-XX:+PrintTenuringDistribution-XX:+PrintAdaptiveSizePolicy-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof

验证效果

调整前 vs 调整后

指标调整前调整后
Young GC频率2-3次/分钟3-4次/分钟
Full GC频率每3-4小时1次0次(2周内)
Full GC时长20-30秒0秒
平均响应时间200ms(波动)150ms(稳定)
99分位响应时间2000ms500ms

GC日志对比

调整前

[Full GC (System.gc()) 23.456s]

调整后

[GC pause (G1 Evacuation Pause) (young) 156ms] [GC pause (G1 Humongous Allocation) (young) 45ms]

政务系统JVM调优要点

1. 业务特点决定配置

社保系统:

  • 查询多:结果集缓存 → 需要更大Old区
  • 批量处理:临时对象多 → 需要更大Young区
  • 7×24小时:不能重启 → 需要稳定GC策略

2. 监控指标

# 日常监控脚本#!/bin/bashPID=$(jps|grepBootstrap|awk'{print $1}')echo"=== JVM监控$(date)==="echo"1. GC统计:"jstat-gcutil$PID10005echo"2. 堆内存:"jmap-heap$PID|grep-A20"Heap Usage"echo"3. 线程数:"jstack$PID|grep"java.lang.Thread.State"|wc-lecho"4. 大对象:"jmap-histo:live$PID|head-20

3. 参数模板

# 社保系统JVM参数模板(JDK8)-Xmx8g-Xms8g-XX:+UseG1GC-XX:MaxGCPauseMillis=200-XX:G1HeapRegionSize=4m-XX:InitiatingHeapOccupancyPercent=45-XX:+DisableExplicitGC-XX:+PrintGCDetails-XX:+PrintGCDateStamps-XX:+PrintTenuringDistribution-Xloggc:/logs/gc.log-XX:+UseGCLogFileRotation-XX:NumberOfGCLogFiles=10-XX:GCLogFileSize=10M-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heapdump.hprof

4. 调优流程

  1. 监控:收集GC日志、堆内存、线程栈
  2. 分析:确定瓶颈(Young/Old区、GC算法、代码问题)
  3. 调整:修改参数,小步快跑
  4. 验证:压测验证,监控效果
  5. 固化:生产部署,持续监控

经验教训

1. Full GC不一定是内存问题

  • System.gc()调用
  • JNI内存泄漏
  • 堆外内存溢出

2. 不要迷信默认参数

JDK默认参数针对通用场景,政务系统有特殊性:

  • 数据量大
  • 并发高
  • 稳定性要求高

3. 调优是持续过程

业务变化需要调整参数:

  • 用户量增长
  • 数据量增加
  • 功能迭代

4. 监控比调优更重要

  • 建立基线(正常状态)
  • 设置告警(异常指标)
  • 定期巡检(主动发现)

技术细节

1. 为什么G1GC适合政务系统?

  • 可预测停顿:设置最大停顿时间,保证业务响应
  • 并发标记:减少STW时间,业务影响小
  • 区域划分:避免内存碎片,长期运行稳定

2. 如何确定堆大小?

# 经验公式总内存=操作系统预留(2G)+ 堆内存 + 堆外内存 + 其他进程# 社保系统示例16G服务器: - 操作系统:2G - JVM堆:8G - 堆外内存:2G(Netty、DirectBuffer) - 数据库连接池:1G - 其他:3G缓冲

3. Young区大小计算

# 根据业务特点短期对象多:Young区占比大(60%-70%) 长期对象多:Old区占比大(60%-70%)# 社保系统:查询结果缓存(长期)+ 临时计算(短期)# 采用平衡策略:Young:Old = 1:1

总结

1. 调优目标

不是"零GC",而是"业务无感知":

  • GC停顿时间 < 业务容忍时间(社保:200ms)
  • GC频率 < 业务波动频率
  • 内存使用率 < 安全阈值(80%)

2. 政务系统特殊性

  • 不能随便重启:参数调整要谨慎
  • 数据不能丢:GC不能导致数据不一致
  • 业务不能停:停顿时间必须可控

3. 从这次调优学到的

  1. 先诊断,后开药:jstat、jmap、GC日志分析
  2. 小步快跑:每次只调整一个参数,观察效果
  3. 监控验证:调整后必须验证,数据说话
  4. 持续优化:业务变化,参数也要变化

"full gc没有了"这句话看似简单,背后是分析、调整、无数次的监控验证。

技术问题好解决,难的是在业务不能停的前提下解决问题。

← 返回列表