一、为什么要学 JVM 调优?—— 线上问题的 “预防针”
系统上线后,常会遇到各种 JVM “故障”:CPU 飙升、请求延迟、内存泄露、GC 频繁导致假死、OOM 直接让系统崩溃…… 这些问题会直接影响用户体验甚至业务连续性。
开发人员学 JVM 调优,就像医生学治病 —— 既要懂 JVM 的 “生理结构”(工作原理),也要掌握 “诊断工具”(定位手段),才能在问题出现时快速解决,避免故障扩大。
二、JVM 的 “正常体征”:什么样的 GC 是健康的?
就像人体有正常体温范围,JVM 也有 “健康 GC 标准”,偏离就可能出问题:
- Young GC(YGC):正常频率几分钟到几十分钟一次,单次耗时几毫秒到几十毫秒,用户无感知;
- Full GC(FGC):正常频率几十分钟到几小时一次,单次耗时几百毫秒,不会频繁打断业务。
三、YGC 的 “风险与优化”:新生代的 “健康管理”
1. 常见风险:高配机器也可能 “水土不服”
YGC 的风险多源于 “配置不合理” 或 “机器升级适配差”,比如:
- 机器升级后遗症:低配置(如 2C4G)升级到高配(如 32C64G),若 E 区(伊甸区)跟着放大,回收大量对象会导致 YGC 的 STW 时间变长(极端时达几秒);同时高并发下 E 区对象增速快,YGC 频繁,系统频繁卡顿。
- 分配不合理:上线前未按 “预期并发量 + 单任务内存需求” 评估,E 区过小导致对象快速填满,YGC 频率飙升。
2. 优化方案:精准匹配 “内存需求”
- 按 “并发量 × 单任务内存” 计算 E 区对象增速,合理设置 E 区大小,平衡 YGC 耗时与频率;
- 高配机器优先用 G1 回收器,通过-XX:MaxGCPauseMillis设置预期停顿时间(如 100ms),避免卡顿。
四、FGC 的 “风险、表现与解决”:老年代的 “紧急抢救”
1. FGC 的危害:系统的 “致命打击”
FGC 会大量占用 CPU,且 STW 时间长,轻则导致系统 “时不时卡死”,重则引发 OOM 让系统崩溃。
2. 典型表现:这些信号要警惕
- 机器 CPU 负载持续过高;
- 监控频繁报 FGC 预警;
- 系统无法处理请求,或响应时间远超正常范围。
3. 常见场景与解决办法
| 场景 | 原因分析 | 解决措施 |
|---|---|---|
| 高并发 / 大数据量导致 FGC 频繁 | YGC 频繁,存活对象多,S 区过小,对象频繁进入老年代 | 用jstat分析内存分配,调整 S 区大小,避免对象提前晋升;优化代码减少大对象创建 |
| 一次性加载大量数据 / 大对象 | 大对象直接进入老年代,快速占满空间 | 拆分数据加载逻辑,避免一次性加载;调整大对象阈值(-XX:PretenureSizeThreshold) |
| 内存泄露 | 无用对象长期驻留老年代,无法回收 | 用jstat初步定位,结合 MAT 分析内存快照,找到泄露代码并修复 |
| 永久代 / 元区域类过多 | 类加载频繁且未回收,触发 FGC | 检查是否存在类加载泄露;JDK8 + 调整元区域大小(-XX:MetaspaceSize) |
| 误调用System.gc() | 代码主动触发 FGC | 加入 JVM 参数-XX:DisableExplicitGC,禁止代码显式触发 FGC |
五、OOM(内存溢出):JVM 的 “致命故障”—— 表象、排查与解决
1. OOM 的典型表象:系统 “崩溃前的信号”
- 应用直接抛出java.lang.OutOfMemoryError异常(日志可查);
- 服务无响应,接口调用超时,甚至进程被操作系统杀死;
- 内存监控显示 JVM 内存使用率持续 100%,GC 频繁但回收效果极差。
2. OOM 的排查思路:一步步找到 “病根”
第一步:确定 OOM 类型
从异常日志判断是哪种内存溢出:
- Java heap space:堆内存不足(最常见,多因对象过多或堆配置过小);
- Metaspace:元区域不足(类太多或元区域配置过小);
- Direct buffer memory:直接内存不足(NIO 操作频繁,未及时释放)。
第二步:抓取内存快照
- 用jmap命令抓取快照:jmap -dump:format=b,file=heapdump.hprof <进程ID>(若 OOM 时自动生成快照,可配置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./);
- 用 MAT(Memory Analyzer Tool)或 JProfiler 分析快照,定位 “内存大户”(如大量未回收的对象、重复创建的大集合)。
第三步:结合 GC 日志与监控
- 查看 GC 日志(配置-Xlog:gc*:file=gc.log:time,level,tags:filecount=5,filesize=100m),分析 GC 频率、回收效率、内存增长趋势;
- 结合监控平台(如 Prometheus+Grafana),看是否有突发流量、异常对象创建高峰。
第四步:定位代码问题
- 若快照显示某类对象异常增多,查看该类的创建逻辑(如是否在循环中重复创建、是否有静态集合未清理);
- 若元区域溢出,检查类加载器是否存在泄露(如自定义类加载器未释放)。
六、JVM 性能调优的 “完整思路”:从设计到上线的全流程
- 开发完成后:提前 “估算内存需求”
- 按 “QPS× 单请求对象大小” 计算 E 区填满时间,确定 YGC 频率;
- 估算 YGC 后存活对象大小、晋升老年代的对象量,预测老年代增速与 FGC 频率;
- 原则:让 YGC 后存活对象远小于 S 区,尽量不让对象进入老年代,减少 FGC。
- 压测阶段:模拟线上 “压力测试”
- 用 JMeter 等工具模拟高并发,通过jstat(如jstat -gc <进程ID> 1000 10)观察指标:E 区增速、YGC 频率 / 耗时、老年代增速、FGC 频率 / 耗时;
- 根据指标调整内存分配(如新生代 / 老年代比例、E 区 / S 区大小)。
- 上线后:持续 “监控与维护”
- 小公司:用jstat定时采集数据并写入文件,每日查看;
- 大公司:上监控平台,监控 “机器资源(CPU / 磁盘 / 内存 / 网络)+JVM 指标(GC 频率 / 内存使用率)+ 业务指标(QPS / 响应时间)+ 异常报错”;
- 发现异常(如 YGC 频繁、FGC 突增),及时介入排查。
- 问题定位:精准 “对症下药”
- 若 YGC 太频繁:检查 E 区是否过小,或代码创建对象过多;
- 若对象频繁晋升老年代:调整 S 区大小、动态年龄判断阈值(-XX:MaxTenuringThreshold);
- 若大对象直接进老年代:调整大对象阈值,或拆分大对象;
- 若 FGC 频繁:排查内存泄露、类加载问题,或调整老年代大小。
七、服务假死:JVM 的 “昏迷状态”—— 排查方法
服务假死(接口无法调用,但未抛 OOM),多因 “GC 频繁” 或 “CPU 耗尽”,排查步骤如下:
- 场景 1:CPU 占比高
- 用top命令按 CPU 排序(输入P),定位高 CPU 进程;
- 用top -Hp <进程ID>查看进程内线程,按 CPU 排序(输入P),定位高 CPU 线程;
- 用printf %x\n <线程ID>将线程 ID 转 16 进制,再用jstack <进程ID> | grep <16进制ID>查看堆栈,找到有问题的代码(如死循环、频繁计算)。
- 场景 2:CPU 占比低但内存高
- 内存高但 CPU 低,说明 GC 回收无效,内存无法释放,可能导致频繁 FGC 或 OOM;
- 用jstat -gc <进程ID>分析:若 GC 频繁但耗时短,且服务无假死,可能是正常高负载;若 GC 频繁且耗时变长,或内存使用率持续 100%,需抓取内存快照(jmap)用 MAT 分析,定位内存泄露或大对象。
八、调优原则与 “未来应对”:长期保障系统稳定
- 核心调优原则
- 尽量让 YGC 后存活对象小于 S 区的 50%,留在新生代,减少对象进入老年代;
- 减少 FGC 频率,避免 FGC 对业务的影响;
- 所有调整需基于监控数据,避免 “拍脑袋” 配置。
- 应对高负载增长(10 倍 / 100 倍)
- 水平扩展:按负载增比增加服务器数量;
- 垂直升级:用更高配置机器(如 32C64G 替代 4C8G),提升处理速度与内存容量;
- 回收器选择:响应时间敏感且内存大的场景,优先用 G1 或 ZGC,减少 STW 影响。