VM 优化是一个系统性工程,既包括事前预防(合理的参数配置、代码规范),也包括事后排查(内存泄漏、CPU飙升等故障的快速定位与解决)。下面从这两个维度展开。
一、JVM 优化通用方案
1. 内存区域参数调优
| 参数 | 作用 | 建议 |
|---|---|---|
-Xms / -Xmx | 堆内存初始/最大值 | 通常设为相同值,避免动态扩容;根据机器内存和业务量设置,如 4G~16G |
-Xmn | 新生代大小 | 一般为堆的 1/3 ~ 1/4,或通过 -XX:NewRatio 设置 |
-XX:MetaspaceSize / -XX:MaxMetaspaceSize | 元空间大小 | 避免频繁扩容,建议 256M~512M |
-Xss | 线程栈大小 | 默认 1M,若线程数极多可适当减小,防止栈溢出 |
-XX:SurvivorRatio | Eden 与 Survivor 比例 | 默认 8,可保持,减少对象过早晋升 |
2. 垃圾回收器选择
- 响应优先(低延迟):G1(
-XX:+UseG1GC)适用于堆内存较大(>4G)且对停顿敏感的场景。 - 吞吐优先:Parallel Scavenge + Parallel Old(
-XX:+UseParallelGC)适合后台计算型任务。 - 极低延迟(<10ms):ZGC(
-XX:+UseZGC)或 Shenandoah,需 JDK 11+。 - CMS 已废弃(JDK 9 后标记废弃,JDK 14 移除),生产环境建议用 G1 替代。
3. 其他重要参数
-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动 dump 堆,便于事后分析。-XX:HeapDumpPath=./heapdump.hprof:指定 dump 文件路径。-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log:记录 GC 日志,便于分析 GC 行为。-XX:+DisableExplicitGC:禁止代码中显式调用System.gc(),避免误触 Full GC。
二、内存泄漏排查与解决
内存泄漏:对象不再被使用却仍然被引用,导致 GC 无法回收,最终引发 OOM 或频繁 GC。
1. 常见原因
- 集合类(
HashMap、ArrayList)未及时清理。 - 监听器、回调未反注册。
- 内部类持有外部类引用(隐式
this$0)。 - 连接池、缓存未设置过期策略。
- 线程局部变量(
ThreadLocal)未调用remove()。 - 静态字段持有大对象或集合。
2. 排查工具与步骤
(1)初步观察
- 使用
jstat -gc <pid> 1000观察 GC 情况。若老年代持续增长且 Full GC 频繁,很可能存在内存泄漏。 - 使用
jmap -histo:live <pid>查看存活对象统计,关注实例数异常高的类。
(2)堆转储分析(关键)
-
触发 OOM 时自动 dump:
-XX:+HeapDumpOnOutOfMemoryError。 -
手动 dump:
jmap -dump:format=b,file=heap.hprof <pid>。 -
分析工具:
- Eclipse MAT(Memory Analyzer Tool):自动生成 Leak Suspects,找出大对象和支配树,分析引用链。
- VisualVM / JProfiler:实时监控对象数量,对比快照。
(3)常见定位思路
- MAT 中查看 Dominator Tree,找出占用内存最大的对象。
- 右键对象 → Merge Shortest Paths to GC Roots → 排除软/弱/虚引用,找到阻止回收的根引用。
- 若怀疑 ThreadLocal,可使用 MAT 的 ThreadLocal 视图或工具
jdk.jcmd的ThreadLocal检查。
(4)VisualVM工具
3. 解决措施
- 修正代码逻辑,确保对象在不再使用时将引用置为
null。 - 使用弱引用(
WeakHashMap)或软引用缓存。 - 为缓存设置 TTL 和最大大小(如 Guava Cache、Caffeine)。
- 线程池、连接池资源使用 try-finally 或 try-with-resources 确保释放。
三、CPU 100% 排查与解决
CPU 100% 通常是代码问题或 JVM 内部频繁操作导致,常见原因:
- 业务代码死循环(无限 while,无 sleep/yield)
- 频繁 GC(尤其是 Full GC)
- 大量线程竞争锁(自旋锁或 synchronized 竞争激烈)
- 正则表达式回溯(导致 CPU 飙升)
1. 排查步骤
(1)确认进程 CPU 高
top或top -Hp <pid>查看哪个进程、哪个线程占用 CPU 高(ps H -eo pid,tid,%cpu | grep <pid>)。- 记录线程 ID(十进制),转换为十六进制(
printf "%x\n" tid)。
(2)抓取线程堆栈
jstack <pid> > jstack.log。- 在
jstack.log中搜索十六进制线程 ID,定位到对应的线程栈。 - 重点关注 RUNNABLE 状态、BLOCKED 状态以及堆栈中的业务代码行号。
(3)快速定位 GC 问题
- 使用
jstat -gcutil <pid> 1000查看 GC 时间占比。若 YGC/FGC 频繁且耗时高,说明 CPU 被 GC 消耗。 - 此时可结合 GC 日志(
-Xloggc:gc.log)分析 GC 类型、原因,判断是否内存不足或内存泄漏。
(4)辅助工具
- Arthas:阿里开源诊断工具,可在线查看线程 CPU 使用率(
thread -n 10),监控方法调用耗时(trace)。 - perf:系统级性能分析工具,可查看内核态和用户态热点函数。
2. 典型场景与解决方案
| 场景 | 表现 | 解决 |
|---|---|---|
| 业务代码死循环 | CPU 长期 100%,堆栈显示业务线程在循环中 | 优化算法,加入适当 Thread.sleep() 或 yield(),避免忙等待 |
| 频繁 Full GC | CPU 高且波动,GC 日志显示 Full GC 频繁 | 分析堆内存,排查内存泄漏;增大堆内存;调整 GC 策略(如改用 G1) |
| 锁竞争激烈 | 大量线程处于 BLOCKED 或 RUNNABLE(等待锁) | 降低锁粒度;使用读写锁;无锁编程(CAS、ConcurrentHashMap) |
| 正则表达式灾难性回溯 | CPU 飙升,堆栈显示 Pattern.matcher | 优化正则表达式,避免嵌套量词;使用 String.indexOf 等替代 |
3. 实战示例(Arthas 快速定位)
bash
# 1. 启动 arthas
java -jar arthas-boot.jar
# 2. 查看 CPU 占用最高的线程
thread -n 5
# 3. 查看具体线程堆栈
thread <thread_id>
# 4. 监控某个方法调用耗时(怀疑死循环的方法)
trace com.example.Service methodName -n 5
四、预防与持续优化
- 代码规范:避免在热点路径创建大对象;及时释放资源;使用池化技术。
- 监控告警:接入 APM(如 SkyWalking、Prometheus + Grafana),监控 JVM 内存、GC 次数、CPU 使用率,设置阈值告警。
- 压测与预发布:上线前进行压力测试,模拟高负载,提前发现内存泄漏或性能瓶颈。
- 定期回顾:定期分析 GC 日志,调优参数;根据业务增长调整堆大小。
以上内容覆盖了 JVM 优化的常见方向和线上故障排查的关键步骤。在实际面试或工作中,重点掌握工具的使用(jstat、jmap、jstack、MAT、Arthas)和问题定位的思维路径,就能从容应对 JVM 相关问题。