# JVM GC调优三板斧——先诊断、再调参、后验证

JVM GC调优三板斧------先诊断、再调参、后验证

背景

政务系统上线后,运维反馈系统偶尔会"卡一下",持续时间不长,但频率不固定。数据库慢SQL排查过了,网络也没问题,服务器资源充足。

这种"说不清道不明"的性能问题,最后往往落在JVM的GC上。

2023年处理过一个类似问题,总结了一套三板斧流程:先用jstat诊断GC原因,再针对性调参,最后验证效果

第一步:诊断------jstat -gccause 看清GC的"病历"

很多人知道用jstat -gcutil看GC统计,但-gcutil只告诉你频率和耗时,不告诉你为什么触发GC。

真正好用的命令是:

bash 复制代码
jstat -gccause <pid> 1000

每秒输出一次GC统计,最后两列是关键:

复制代码
  S0     S1     E      O      M     CCS    YGC   YGCT   FGC   FGCT   GCT    LGCC                 GCC
  0.00   0.00  68.23  45.67  92.13  88.45   234   2.345     5   6.789  9.134 Allocation Failure   No GC
  0.00   0.00  12.34  46.12  92.13  88.45   235   2.378     5   6.789  9.167 Allocation Failure   No GC
  0.00   0.00  78.90  46.89  92.13  88.45   236   2.412     6   8.234  10.646 Allocation Failure   Full GC (Metadata GC Threshold)

重点看最后两列:

含义 怎么读
LGCC 上一次GC的原因 最近一次GC为什么触发
GCC 当前GC的原因 现在正在进行的GC为什么触发(No GC表示空闲)

常见GC原因对照:

LGCC/GCC的值 含义 怎么办
Allocation Failure 新生代分配失败,正常YGC 频率太高就调大新生代
Full GC (Metadata GC Threshold) Metaspace满了 调大Metaspace
Full GC (System.gc()) 代码显式调了System.gc() 加-XX:+DisableExplicitGC
Full GC (Ergonomics) JDK自适应触发 看老年代使用率,可能堆太小
Full GC (Heap Dump Initiated GC) 做了heap dump 排查dump的来源
Full GC (Last ditch collection) CMS/G1老年代回收失败兜底 严重了,堆内存根本不够用

盯了几分钟,如果发现Full GC频繁且原因明确(比如Metaspace不够),调参就有了方向。

第二步:调参------对症下药

诊断出原因后,针对性调整JVM参数。不要上来就抄网上的"JVM调优最佳实践"------不同系统的GC特征不一样,参数也不一样。

场景一:Allocation Failure过于频繁

说明新生代太小,对象还没用多久就被YGC回收,或者晋升到老年代太快。

复制代码
# 调大新生代(默认新生代占整个堆的1/3)
-Xmn512m

# 或者调整新生代与老年代比例
-XX:NewRatio=2   # 新生代:老年代 = 1:2

# 如果大对象直接进老年代,调大这个阈值
-XX:PretenureSizeThreshold=1m

场景二:Metaspace触发Full GC

政务系统经常加载大量动态生成的JSP、反射类,Metaspace压力大。

复制代码
# 调大Metaspace(默认256m,政务系统建议512m~1g)
-XX:MaxMetaspaceSize=512m

# 同时调大压缩类空间
-XX:CompressedClassSpaceSize=256m

场景三:Full GC (Ergonomics) 频繁

说明老年代增长太快,对象存活时间太长或者内存泄漏。

复制代码
# 调大整个堆
-Xmx4g

# 如果用的CMS,可以调整触发老年代回收的阈值
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly

# 如果用的G1,调整暂停时间目标
-XX:MaxGCPauseMillis=200

场景四:System.gc()作祟

参考另一篇博客《System.gc()的隐蔽陷阱》,直接加-XX:+DisableExplicitGC

第三步:验证------调了之后到底有没有用

参数改完不是万事大吉,必须验证。重启后继续盯着jstat -gccause,对比调参前后的数据:

bash 复制代码
# 调参前记录5分钟数据(导出到文件)
jstat -gccause <pid> 1000 > gc_before.log &

# 调参后记录5分钟数据
jstat -gccause <pid> 1000 > gc_after.log &

关注这几个指标:

指标 调参前 调参后 评判
FGC(Full GC次数/分钟) 3~5次 0次 消除Full GC
FGCT(Full GC总耗时) 累计增长快 不再增长 Full GC消除
YGC频率 过高(>10次/秒) 合理(1~5次/秒) 新生代分配正常
O(老年代使用率) 持续增长 稳定 没有内存泄漏

调参成功的标志:Full GC没有了。

一个健康的JVM,Full GC应该极少甚至没有。如果YGC频率高但每次都很快回收(LGCC一直是Allocation Failure,E区没有飙升到90%+),那是正常的,不用管。

实战案例:一体化平台GC调优

2023年给一个政务一体化平台做GC调优,过程如下:

诊断阶段

jstat -gccause盯了10分钟,发现:

  • YGC每秒3~4次------频率偏高
  • 每小时触发2~3次Full GC------LGCC显示Ergonomics
  • 老年代使用率每次YGC后都会涨一点,说明对象晋升太快

调参阶段

原始参数(.tomcat的setenv.sh):

bash 复制代码
JAVA_OPTS="-Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m"

调优后参数:

bash 复制代码
JAVA_OPTS="-Xms4g -Xmx4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m -XX:NewRatio=1 -XX:+DisableExplicitGC"

关键改动:

  • 堆从2G扩到4G------应用服务器内存够用,何必省
  • Metaspace从256M扩到512M------政务系统JSP和动态类多
  • 新生代比例从1:2调到1:1------大量短生命周期对象在新生代就回收掉
  • 禁止System.gc()------防止第三方库触发无谓的Full GC

验证阶段

调参后运行一周,jstat -gccause显示:

  • YGC降到每秒1~2次
  • FGC次数:0------Full GC消失了
  • 老年代使用率稳定在35%左右,不再持续攀升

系统再也没有"卡一下"的现象。

常见误区

误区一:一上来就调G1

有些文章说"G1比CMS好,生产环境应该用G1"。不一定。如果你的堆不大(<8G),CMS或者甚至Parallel GC就够了。G1的目标是低延迟大堆,小堆用G1反而开销更大。

复制代码
# 4G以下的堆,Parallel GC可能就够了(JDK 8默认)
# 4G~16G,CMS或G1都可以
# 16G以上,推荐G1

误区二:把Xms和Xmx设成不一样的

复制代码
# 不好:Xms2g -Xmx4g
# 好:Xms4g -Xmx4g

Xms和Xmx不一样,JVM运行过程中会动态扩缩堆,扩容本身要Full GC来整理内存。既然服务器内存够用,就让它一开始就分配满,省去扩容的开销。

误区三:调了参数不管

见过有人在测试环境调了参数,发现"好像好一点"就上线了。没有量化数据支撑的调优等于白调。一定要用jstat -gccause前后对比,拿数据说话。

命令速查

bash 复制代码
# 诊断GC原因(最有用)
jstat -gccause <pid> 1000

# 查看GC统计概览
jstat -gcutil <pid> 1000

# 查看详细GC统计(含各区域大小)
jstat -gc <pid> 1000

# 查看JVM当前参数
jinfo -flags <pid>

# 查看堆内存分布
jmap -heap <pid>

# 导出heap dump分析内存泄漏
jmap -dump:format=b,file=heap.hprof <pid>

总结

JVM GC调优不神秘,就三步:

  1. 先诊断 :用jstat -gccause看清GC原因,不要猜
  2. 再调参:对症下药,不要抄网上的"万能参数"
  3. 后验证:用数据对比调参前后效果,不要凭感觉

政务系统的JVM问题,80%是这三个原因:堆太小、Metaspace不够、System.gc()作祟。把这三个排查完,基本就解决了。


感谢豆包、智谱、OpenCode在写作过程中的辅助。

相关推荐
2201_761040592 小时前
Layui layer.tips提示框怎么设置方向和颜色
jvm·数据库·python
weixin_424999362 小时前
C#怎么实现EF Core迁移 C#如何用Entity Framework Core进行数据库迁移和更新表结构【数据库】
jvm·数据库·python
m0_743623922 小时前
安装宝塔面板提示缺少必要的依赖包_批量安装系统库
jvm·数据库·python
qq_334563552 小时前
SQL提升开发效率_使用CTE重构嵌套子查询逻辑
jvm·数据库·python
qq_654366982 小时前
CSS如何处理超长文本换行问题_结合word-wrap属性
jvm·数据库·python
u0109147602 小时前
Go语言怎么做分布式缓存_Go语言分布式缓存教程【经典】
jvm·数据库·python
2301_817672262 小时前
mysql如何通过调整Undo Log优化并发性能_优化innodb_max_undo_log_size
jvm·数据库·python
2301_777599372 小时前
mysql如何配置插件以提升查询性能_安装启用memcached插件
jvm·数据库·python
2301_773553622 小时前
Tailwind CSS如何实现固定定位布局_使用fixed与z-index控制CSS层级
jvm·数据库·python