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%。
分析:
jmap -histo:live
→ 发现大量OrderDTO
,CartService$TempCache
------ 短命对象!jstat -gc
→ Survivor 使用率 95% ------ 放不下,直接晋升- GC 日志 → Full GC 前老年代占用 98% ------ 被短命对象撑爆
调优:
-
扩大新生代 :
bash-Xmn3g # 从 1g → 3g
-
延长对象熬代时间 :
bash-XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=20
-
换 G1,设停顿目标 :
bash-XX:+UseG1GC -XX:MaxGCPauseMillis=100
-
关闭动态调整,手动控制更稳 :
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 日志,用工具对比验证 |