JVM 调优参数与实战

举报
yd_287908631 发表于 2026/10/31 15:21:29 2026/10/31
【摘要】 按场景给出可落地的 JVM 参数模板与验证方法

JVM 调优不是背参数表,而是先明确目标(吞吐、延迟还是内存占用),再用监控数据判断瓶颈,最后用最小改动验证效果。多数线上问题的根因是代码而非参数,调参只能缓解不能治愈。本文给出一套从基线配置到验证闭环的方法,并区分 JDK 8 与 JDK 17 的差异。

先确定调优目标

三个目标互相制约,必须选一个主目标:

| 目标 | 关注指标 | 典型场景 | 参数倾向 |

| --- | --- | --- | --- |

| 吞吐优先 | 单位时间处理量、GC 总耗时占比 | 批处理、离线计算 | 大堆、Parallel GC |

| 延迟优先 | P99 / P999 响应时间、单次 GC 停顿 | 在线 API、交易 | 小堆、G1 或 ZGC |

| 内存优先 | RSS、堆占用 | 容器密度、成本敏感 | 小堆、及时归还 |

提示:绝大多数 Web 服务属于延迟优先,但延迟目标要看业务 SLA。P99 要求 200 ms 的服务,GC 停顿控制在 50 ms 以内即可,没必要为了 5 ms 停顿引入 ZGC 的额外 CPU 开销。

内存相关参数

堆大小

-Xms4g -Xmx4g

-Xms 与 -Xmx 设成相同值是生产环境的默认做法。堆动态扩容会触发 Full GC,且扩容后不会自动归还,形成「只涨不降」的假象。设成固定值后,内存曲线更容易解读。

堆大小怎么定?一个实用起点是「Full GC 后老年代存活对象的 3 到 4 倍」。存活 1 GB,堆就设 3 到 4 GB。堆过大则单次 GC 停顿变长,堆过小则 GC 频繁。

新生代

# G1 下用比例控制
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60

# Parallel / CMS 下用绝对大小或比例
-Xmn1g
-XX:NewRatio=2

G1 不推荐设置 -Xmn,会禁用停顿预测模型。-XX:SurvivorRatio=8 控制 Eden 与单个 Survivor 的比例,默认 8 表示 8:1:1。

元空间

-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

MetaspaceSize 是首次触发元空间 GC 的阈值,不是初始容量。MaxMetaspaceSize 必须显式设置,否则元空间可以吃到物理内存耗尽。Spring Boot 应用通常 256 MB 足够,大量使用 CGLIB、Groovy、JSP 的应用需要放宽。

JDK 8 的永久代参数 -XX:PermSize / -XX:MaxPermSize 在 JDK 8 中已被忽略(JDK 8 移除了永久代),JDK 17 上更是完全无效。如果启动日志出现 Ignoring option PermSize,说明配置没有清理。

栈与直接内存

-Xss1m
-XX:MaxDirectMemorySize=512m

-Xss 影响每个线程的栈大小,线程数乘以它才是总占用。200 个线程配 1 MB 是 200 MB,容易被忽略。MaxDirectMemorySize 未设置时默认等于 -Xmx,Netty 应用必须显式限制。

收集器选择

# JDK 8:G1 需显式开启
-XX:+UseG1GC -XX:MaxGCPauseMillis=200

# JDK 17:G1 已是默认,可省略
-XX:MaxGCPauseMillis=200

# 超低延迟场景(JDK 17)
-XX:+UseZGC -XX:ZCollectionInterval=5

JDK 8 默认是 Parallel Scavenge + Parallel Old,JDK 9 起默认 G1。CMS 在 JDK 9 被标记废弃,JDK 14 移除,因此 JDK 17 上 -XX:+UseConcMarkSweepGC 会直接启动失败:

Unrecognized VM option 'UseConcMarkSweepGC'
Error: Could not create the Java Virtual Machine.

从 JDK 8 迁移到 JDK 17 时,必须删掉所有 CMS 相关参数,包括 -XX:+UseCMSInitiatingOccupancyOnly、-XX:CMSInitiatingOccupancyFraction、-XX:+CMSParallelRemarkEnabled。

日志与诊断参数

JDK 9 起 GC 日志改用统一日志框架:

-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m

JDK 8 的写法是:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/app/gc.log \
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M

生产环境建议常开以下参数:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-XX:ErrorFile=/data/dump/hs_err_pid%p.log
-Xlog:safepoint=info:file=/var/log/app/safepoint.log:time,uptime

HeapDumpOnOutOfMemoryError 是 OOM 时唯一能保留现场的手段。ErrorFile 保存 JVM 崩溃时的 hs_err_pid 文件,包含崩溃线程、寄存器、内存映射,是排查 native 崩溃的关键。

注意:-XX:+PrintGCDetails 在 JDK 9 到 JDK 17 之间还能用,但会打印弃用警告,JDK 18 起部分参数被移除。新项目直接使用 -Xlog。

容器化环境

JDK 8u191 之前不识别容器 cgroup 限制,Runtime.getRuntime().maxMemory() 返回的是宿主机内存,导致堆按宿主机大小自动扩张,最终被 OOMKilled。JDK 8u191+ 和 JDK 10+ 支持 -XX:+UseContainerSupport(默认开启)。

即使支持容器感知,也建议显式指定堆:

# 容器 limit 2 GiB
java -XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=70.0 \
     -XX:MaxMetaspaceSize=256m \
     -XX:MaxDirectMemorySize=256m \
     -jar app.jar

MaxRAMPercentage 按容器 limit 计算,70% 留给堆,剩余 30% 覆盖元空间、线程栈、Code Cache、GC 结构和 glibc 开销。经验值是 60% 到 75%,堆越大比例可以越高。

不建议在容器中同时使用 -Xmx 和 MaxRAMPercentage,后者会被前者覆盖,容易造成误解。

一份可用的基线模板

JDK 17 + Spring Boot 3.x + 容器 4 GiB:

java \
  -XX:MaxRAMPercentage=70.0 \
  -XX:InitialRAMPercentage=70.0 \
  -XX:MaxMetaspaceSize=384m \
  -XX:MetaspaceSize=256m \
  -XX:MaxDirectMemorySize=384m \
  -Xss512k \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:G1HeapRegionSize=4m \
  -XX:InitiatingHeapOccupancyPercent=40 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dump \
  -XX:ErrorFile=/data/dump/hs_err_pid%p.log \
  -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \
  -Xlog:safepoint=info:file=/var/log/app/safepoint.log:time,uptime \
  -jar app.jar

JDK 8 的等价模板需要把 -Xlog 换成 -XX:+PrintGCDetails 系列,并显式加 -XX:+UseG1GC。

用数据验证调优效果

调参后至少观察一个完整的业务高峰周期,对比以下指标:

# 每秒采样,观察 60 秒
jstat -gcutil 12345 1000 60

# 汇总 GC 停顿分布
jcmd 12345 GC.heap_info

关键判断标准:

  • Young GC 频率低于每 10 秒一次,单次低于 50 ms。
  • Full GC 次数为 0,或每天不超过 1 次。
  • 老年代使用率在 Full GC 后能回落到 30% 以下。
  • 应用 P99 延迟没有因 GC 出现尖峰。

如果调参后 Full GC 仍然频繁,说明是内存泄漏或对象创建速率问题,应回到代码层排查,而不是继续加堆。

一次真实的调优过程

现象

某订单查询服务,JDK 17,容器 4 GiB,原参数只有 -Xmx3g。监控显示 P99 从 180 ms 涨到 620 ms,每天有 2 到 3 次持续 3 秒左右的尖峰。

定位

jstat -gcutil 12345 1000 30
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  91.24  67.18  78.33  96.12  93.44  12033  601.650    46  138.000  739.650

三个信号同时出现:YGC 高达 12033 次,平均每 8 秒一次;FGC 46 次,平均每次 3 秒;元空间 M 达到 96%,已经接近无上限增长的边界。

GC 日志里找到 Full GC 的成因:

[info][gc] GC(12079) Pause Full (G1 Compaction Pause) 2861M->1204M(3072M) 3021.447ms
[info][gc,metaspace] GC(12079) Metaspace: 421880K(425984K)->421880K(425984K)

这是 G1 Compaction Pause,说明 G1 已经退化到串行 Full GC。老年代回收后仍有 1.2 GB,存活对象稳定,不是泄漏;但回收前 2.8 GB,说明晋升速率过高。

根因

查看晋升行为:

jstat -gc 12345 1000 5

S0C / S1C 各约 20 MB,EC 约 300 MB。新生代总占比不到 11%,而 YGCT 占总运行时间的比例极高。结合业务特征——批量导入时每秒创建数十万个临时 DTO——结论是新生代太小,临时对象来不及在 Young GC 中被回收就被晋升到老年代,迅速填满老年代并触发 Mixed GC 和 Full GC。

元空间 96% 则是因为大量使用 CGLIB 代理,且没有设置 MaxMetaspaceSize,元空间随类生成持续增长。

调整

-XX:MaxRAMPercentage=70.0 \
-XX:MaxMetaspaceSize=384m -XX:MetaspaceSize=256m \
-XX:MaxDirectMemorySize=256m \
-XX:MaxGCPauseMillis=200 \
-XX:G1NewSizePercent=20 -XX:G1MaxNewSizePercent=60 \
-XX:InitiatingHeapOccupancyPercent=40

改动点与依据:

  • 显式限制元空间,避免它挤占堆外余量。
  • 提高新生代下限到 20%,给临时对象足够的回收空间,降低晋升速率。
  • 提前触发并发标记(40%),让 Mixed GC 在老年代填满前开始工作。

结果

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  88.31  42.06  51.28  62.44  60.11   2841  142.220     0    0.000  142.220

YGC 从 12033 降到 2841,FGC 归零,P99 回落到 210 ms。注意这不是靠加堆实现的——堆总量没变,只是让对象在新生代被回收掉。

常见坑 / 最佳实践

  1. 照搬网上的参数模板。堆大小、Region 大小、暂停目标都与具体负载强相关。模板只能作为起点,必须用自己的 GC 日志验证。
  2. 同时设置 -Xmn 和 G1 的百分比参数。两者冲突,-Xmn 优先且会禁用动态调整,导致停顿预测失效。
  3. 把 MaxGCPauseMillis 当成硬保证。它是软目标,G1 通过调整回收 Region 数量逼近它。设置过小会导致回收频率暴涨、吞吐下降。
  4. 在 JDK 17 上保留 CMS 参数。JDK 14 已移除 CMS,任何 CMS 参数都会导致 JVM 启动失败,迁移前必须清理启动脚本和容器镜像中的旧参数。
  5. 只调堆不调堆外。Netty、RocketMQ 客户端、压缩库都使用直接内存,容器 OOMKilled 时 heap dump 里一切正常,问题在堆外。
  6. 调优后不保留对比数据。没有调优前的基线,就无法证明改动有效。上线前记录 GC 次数、停顿分布、P99 延迟,改动后逐项对比。

小结

  • 调优从目标开始:吞吐、延迟、内存三者选主目标,再选收集器和参数。
  • -Xms 与 -Xmx 保持一致,堆大小以 Full GC 后存活对象的 3 到 4 倍为起点。
  • G1 下用百分比控制新生代,不要用 -Xmn;MaxGCPauseMillis 是软目标。
  • JDK 17 已移除 CMS,迁移时必须清理相关参数,否则 JVM 无法启动。
  • 容器环境用 MaxRAMPercentage 并把 25% 到 40% 留给堆外,同时常开堆转储和 GC 日志。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。