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

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

一句话总结
内存调优 = 调"对象生命周期分布"与"GC行为节奏"的匹配度。不是调"内存越大越好",也不是只调"新生代/老年代比例",而是从空间结构、晋升策略、GC算法、停顿目标四层入手,让短命对象在新生代快速回收,长命对象在老年代稳定存放,最终实现少 Full GC、低延迟、高吞吐、零 OOM。


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

很多人以为"调优 = 调 -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 飙高

bash 复制代码
-Xms4g -Xmx4g -Xmn512m  # 新生代仅 512M

→ Eden 秒满,每秒多次 Minor GC;

→ 虽单次快,但累积 STW 时间长,CPU 被 GC 线程吃满;

→ Survivor 放不下存活对象,大量短命对象提前晋升老年代

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

bash 复制代码
-Xmn3g -Xmx4g  # 老年代只剩 1G

→ 缓存、连接池、大对象稍多就撑满;

→ Full GC 触发,STW 1~5 秒,用户请求超时;

→ 如果用 CMS,还会触发 Concurrent Mode Failure,更卡。

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

bash 复制代码
-XX:MaxTenuringThreshold=1  # 活一次就进老年代

→ 本该在新生代死的对象,全跑老年代去了;

→ Full GC 时要扫描/移动这些"垃圾",效率极低;

→ Full GC 时间变长,频率变高。

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

你问:"默认不是新生代复制、老年代整理吗?为什么不能用 Serial?"

关键纠正:

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

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

🚫 "默认策略"不是固定算法,而是 JVM 根据堆大小自动选择的 GC 实现:

  • JDK 8:堆 < ~4G → Serial;堆 ≥ ~4G → Parallel;
  • JDK 9+:堆 < ~4G → Serial;堆 ≥ ~4G → G1; → 它根本不关心你的业务是"低延迟"还是"高吞吐"

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


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

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

必须监控的核心指标:

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

示例 jstat:

bash 复制代码
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 请求、临时计算)→ 扩大新生代

    bash 复制代码
    -Xmn2g  # 总堆 4G 时,新生代占一半
  • 长命对象多(缓存、静态数据)→ 扩大老年代

    bash 复制代码
    -XX:NewRatio=3  # 老年代 : 新生代 = 3:1
  • 对象"中等寿命"(几秒~几分钟)→ 调大 Survivor,增加熬代次数

    bash 复制代码
    -XX:SurvivorRatio=6     # Eden:S0:S1 = 6:1:1 (默认 8:1:1)
    -XX:MaxTenuringThreshold=20  # 默认 15,可适当调高

目标:让短命对象在新生代"自然死亡",别进老年代。


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

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

不要迷信"最新",要匹配"需求"。G1 不是万能,Parallel 不是落后。


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

调完必须验证:

  • 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 日志(必备):

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

四、实战案例:电商下单接口调优全过程

问题:

  • 接口 P99 偶尔飙到 2s+;
  • 日志发现 Full GC 每 10 分钟一次,停顿 1.5s;
  • jstat 显示老年代每次 GC 后只回收 10%。

分析:

  1. jmap -histo:live → 发现大量 OrderDTO, CartService$TempCache ------ 短命对象!
  2. jstat -gc → Survivor 使用率 95% ------ 放不下,直接晋升
  3. GC 日志 → Full GC 前老年代占用 98% ------ 被短命对象撑爆

调优:

  1. 扩大新生代

    bash 复制代码
    -Xmn3g  # 从 1g → 3g
  2. 延长对象熬代时间

    bash 复制代码
    -XX:SurvivorRatio=6
    -XX:MaxTenuringThreshold=20
  3. 换 G1,设停顿目标

    bash 复制代码
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=100
  4. 关闭动态调整,手动控制更稳

    bash 复制代码
    -XX:-UseAdaptiveSizePolicy

结果:

  • Full GC 从 10分钟/次 → 2小时/次(基本消失);
  • Minor GC 从 5秒/次 → 15秒/次,耗时 50ms → 30ms;
  • 接口 P99 从 2s+ → 80ms;
  • CPU 使用率下降 15%。

调优成功:不是加内存,而是让对象在正确的地方、用正确的方式死掉。


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

错误做法 后果 正确做法
只调 -Xmx,不调 -Xmn 新生代比例失调,GC 更频繁 同步调整新生代大小,保持合理比例
SurvivorRatio 设太小(如 2) Survivor 太小,对象秒晋升 保持默认 8 或微调到 6~7
MaxTenuringThreshold 设 0 或 1 对象"秒进"老年代,污染严重 至少设 5 以上,观察年龄分布后再调
业务敏感还用 Parallel GC STW 太长,请求超时 换 G1 或 ZGC,设 MaxGCPauseMillis
不看 GC 日志,盲目调参 调了也不知道有没有用 必须开启 GC 日志,用工具对比验证
相关推荐
程序员海军2 小时前
如何让AI真正理解你的需求
前端·后端·aigc
yinke小琪2 小时前
说说Java 中 Object 类的常用的几个方法?详细的讲解一下
java·后端·面试
回家路上绕了弯2 小时前
主从架构选型指南:从原理到落地,搞懂怎么选才适合你的业务
后端·架构
该用户已不存在2 小时前
Rust Web框架大比拼:Actix vs Axum vs Rocket,别再只看跑分了
后端·rust
OneWind2 小时前
使用CloudFlare R2上传图片慢怎么解决
后端
River4162 小时前
Javer 学 c++(十六):对象特性篇(上)
c++·后端
文心快码BaiduComate2 小时前
轻松实践:用Python实现“名字大作战”游戏,表白Zulu!
前端·后端·微信小程序
bobz9652 小时前
tc 的锁问题
后端
空想兔2 小时前
JeecgBoot SkyWalking 分布式链路跟踪配置
后端·elasticsearch