JVM 内存调优:到底在调什么?怎么调?


一、调的是什么?—— 四层结构,层层递进

很多人以为“调优 = 调 -Xmx”,这是最表层的理解。

真正完整的调优包含四层:

层级调整内容目标工具/参数
第一层:空间结构新生代/老年代大小、Survivor 比例控制对象流动节奏,避免过早晋升或空间浪费-Xmn, -XX:NewRatio, -XX:SurvivorRatio
第二层:晋升策略对象在新生代“熬”几次 GC 才进老年代过滤短命对象,减轻老年代压力-XX:MaxTenuringThreshold, -XX:+UseAdaptiveSizePolicy
第三层:GC算法选择 Serial / Parallel / G1 / ZGC匹配业务对“延迟”或“吞吐”的需求-XX:+UseG1GC, -XX:+UseZGC
第四层:行为目标设定最大停顿时间、Region大小、回收粒度精细化控制 GC 行为,逼近业务 SLA-XX:MaxGCPauseMillis, -XX:G1HeapRegionSize

二、为什么要调?—— 不调的四种典型死法

死法 1:新生代太小 → Minor GC 频繁 + CPU 飙高

-Xms4g -Xmx4g -Xmn512m  # 新生代仅 512M

→ Eden 秒满,每秒多次 Minor GC;
→ 虽单次快,但累积 STW 时间长,CPU 被 GC 线程吃满;
→ Survivor 放不下存活对象,大量短命对象提前晋升老年代

死法 2:老年代太小 → Full GC 频繁 + 服务卡顿

-Xmn3g -Xmx4g  # 老年代只剩 1G

→ 缓存、连接池、大对象稍多就撑满;
→ Full GC 触发,STW 1~5 秒,用户请求超时;
→ 如果用 CMS,还会触发 Concurrent Mode Failure,更卡。

死法 3:晋升太快 → 老年代被“垃圾”污染

-XX:MaxTenuringThreshold=1  # 活一次就进老年代

→ 本该在新生代死的对象,全跑老年代去了;
→ Full GC 时要扫描/移动这些“垃圾”,效率极低;
→ Full GC 时间变长,频率变高。

死法 4:选错 GC 算法 → 业务需求与 GC 行为错配

关键纠正:

算法思想相同,但“执行引擎”不同 —— 这才是核心差异。

场景错误选择后果正确选择
高并发 Web 服务用 Serial GC单线程 STW,每次 GC 全服务卡住G1 / ZGC(并发、低停顿)
大数据批处理用 CMS碎片多 + Full GC 频繁,总耗时更长Parallel GC(吞吐优先)
大堆低延迟用 Parallel GCSTW 太长,无法满足 SLAZGC / Shenandoah(亚毫秒停顿)

不调优 = 让 JVM 用“猜的策略”硬扛你的业务 → 轻则性能差,重则 OOM 崩溃。


三、怎么调?—— 四步闭环,从监控到验证

第一步:看监控 —— 找出瓶颈在哪

必须监控的核心指标:

指标工具说明
Minor GC 频率 & 耗时jstat -gc <pid>频繁 or 耗时长 → 调新生代大小 or GC 算法
Full GC 频率 & 耗时jstat -gc <pid>频繁 → 老年代太小 or 晋升太快 or GC 算法错
老年代使用率曲线Grafana / VisualVM是否平稳?还是阶梯式暴涨?
每次 GC 后老年代回收率GC 日志回收率低 → 老年代全是“真长期对象” or 被短命对象污染

示例 jstat:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       YGC     YGCT    FGC    FGCT
10752.0 10752.0  0.0   8960.0 65536.0  65536.0  175104.0   174890.1   123    2.452   5      8.732

→ EU 满 → 触发 YGC;OU 接近 OC → 危险,快 Full GC。


第二步:调结构 —— 根据对象生命周期调整空间

原则:
  • 短命对象多(Web 请求、临时计算)→ 扩大新生代
    -Xmn2g  # 总堆 4G 时,新生代占一半
    
  • 长命对象多(缓存、静态数据)→ 扩大老年代
    -XX:NewRatio=3  # 老年代 : 新生代 = 3:1
    
  • 对象“中等寿命”(几秒~几分钟)→ 调大 Survivor,增加熬代次数
    -XX:SurvivorRatio=6     # Eden:S0:S1 = 6:1:1 (默认 8:1:1)
    -XX:MaxTenuringThreshold=20  # 默认 15,可适当调高
    

第三步:选算法 + 设目标 —— 匹配业务需求

业务类型推荐 GC关键参数理由
高并发 Web / APIG1GC-XX:MaxGCPauseMillis=100可控停顿,Region 回收,适合大堆低延迟
大数据批处理Parallel GC无特殊参数最大吞吐量,STW 长但总时间短
超大堆 + 极致低延迟ZGC-XX:+UseZGC -Xmx32g亚毫秒停顿,适合 >16G 堆
客户端 / 小应用Serial GC单线程,开销小,适合 <4G 堆

第四步:验效果 —— 必须对比调优前后数据

调完必须验证:

  • Minor GC 次数是否下降?耗时是否缩短?
  • Full GC 是否减少 or 消失?
  • 平均/P99 响应时间是否改善?
  • 吞吐量(TPS/QPS)是否提升?
  • 是否出现新问题(如晋升失败、Concurrent Mode Failure)?

工具:

  • jstat -gcutil <pid> 1000:实时看 GC 利用率;
  • jmap -histo:live <pid>:看存活对象类型分布;
  • GC 日志分析(GCeasy / gceasy.io):可视化对比,看回收率、停顿分布。

开启 GC 日志(必备):

-Xloggc:/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution  # 看对象年龄分布,关键!

四、避坑指南:五大高频错误调法

错误做法后果正确做法
只调 -Xmx,不调 -Xmn新生代比例失调,GC 更频繁同步调整新生代大小,保持合理比例
SurvivorRatio 设太小(如 2)Survivor 太小,对象秒晋升保持默认 8 或微调到 6~7
MaxTenuringThreshold 设 0 或 1对象“秒进”老年代,污染严重至少设 5 以上,观察年龄分布后再调
业务敏感还用 Parallel GCSTW 太长,请求超时换 G1 或 ZGC,设 MaxGCPauseMillis
不看 GC 日志,盲目调参调了也不知道有没有用必须开启 GC 日志,用工具对比验证
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]