jvm性能调优、内存泄漏、cpu飙升

0 阅读5分钟

VM 优化是一个系统性工程,既包括事前预防(合理的参数配置、代码规范),也包括事后排查(内存泄漏、CPU飙升等故障的快速定位与解决)。下面从这两个维度展开。


一、JVM 优化通用方案

1. 内存区域参数调优

参数作用建议
-Xms / -Xmx堆内存初始/最大值通常设为相同值,避免动态扩容;根据机器内存和业务量设置,如 4G~16G
-Xmn新生代大小一般为堆的 1/3 ~ 1/4,或通过 -XX:NewRatio 设置
-XX:MetaspaceSize / -XX:MaxMetaspaceSize元空间大小避免频繁扩容,建议 256M~512M
-Xss线程栈大小默认 1M,若线程数极多可适当减小,防止栈溢出
-XX:SurvivorRatioEden 与 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. 常见原因

  • 集合类(HashMapArrayList)未及时清理。
  • 监听器、回调未反注册。
  • 内部类持有外部类引用(隐式 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工具

image.png

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 GCCPU 高且波动,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 相关问题。