排查 Java 应用内存泄漏或 CPU 消耗过高的问题需要系统化的方法,结合工具和分析手段。以下是详细的排查步骤和建议:
一、排查内存泄漏
内存泄漏通常表现为堆内存持续增长,最终导致 OutOfMemoryError 或性能下降。以下是排查步骤:
1. 确认内存泄漏
- 现象:观察应用内存使用情况,长时间运行后堆内存持续增长,未被垃圾回收。
- 工具:
- 使用 jvisualvm(JDK 自带)或 VisualVM 监控堆内存使用情况。
- 配置 JVM 参数(如 -XX:+HeapDumpOnOutOfMemoryError)生成堆转储文件(Heap Dump)。
- 使用监控工具(如 Prometheus + Grafana 或商业工具如 New Relic)查看内存趋势。
2. 获取堆转储文件
- 手动生成:
- 使用 jmap:jmap -dump:live,format=b,file=heapdump.hprof <pid>
- 使用 jvisualvm 或 jconsole 触发堆转储。
- 自动生成:配置 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump。
3. 分析堆转储
- 工具:
- Eclipse MAT (Memory Analyzer Tool):分析堆转储,识别占用内存最多的对象和引用链。
- VisualVM:查看对象分布。
- JProfiler 或 YourKit:商业工具,提供更详细的分析。
- 分析重点:
- 查看 Histogram,找出占用内存最多的类或对象。
- 检查 Dominator Tree,定位哪些对象持有大量内存。
- 分析 GC Roots(如 Thread、ClassLoader、Static 字段)到可疑对象的引用链,找出未释放的原因。
- 常见问题:
- 未关闭的资源(如数据库连接、文件流)。
- 缓存未清理(如 HashMap 或 Guava Cache 未设置过期)。
- 静态集合(如 static List)持续累积对象。
4. 检查代码和配置
- 代码审查:
- 检查集合使用:如 List、Map 是否未清理。
- 检查线程池:线程未销毁可能导致对象引用未释放。
- 检查事件监听器:未移除的监听器可能导致对象滞留。
- 配置优化:
- 调整缓存大小或过期策略。
- 检查第三方库是否存在已知内存泄漏问题。
5. 验证修复
- 修复后,重复运行应用并监控内存使用情况,确认泄漏是否解决。
- 使用压力测试工具(如 JMeter)模拟高负载场景,验证修复效果。
二、排查 CPU 消耗过高
高 CPU 使用率可能由死循环、复杂计算、频繁 GC 或线程竞争引起。以下是排查步骤:
1. 确认 CPU 高消耗
- 工具:
- Linux:使用 top 或 htop 查看进程 CPU 使用率,记录 Java 进程 PID。
- Windows:任务管理器或资源监视器查看 CPU 占用。
- 使用 jvisualvm 或 JConsole 监控 CPU 使用情况。
- 现象:CPU 使用率持续高(如接近 100%),或周期性高峰。
2. 获取线程转储
- 方法: COVID-19 - 使用 jstack:jstack <pid> > threaddump.txt 生成线程转储。
- 使用 jvisualvm:查看线程状态和堆栈。
- 使用 top -H -p <pid>(Linux)查看具体线程的 CPU 占用,再用 printf "%x\n" <tid> 将线程 ID 转换为十六进制,匹配 jstack 输出中的 nid。
- 多次采样:每隔 5-10 秒生成几次线程转储,分析线程状态变化。
3. 分析线程转储
- 关注点:
- RUNNABLE 线程:检查是否存在死循环或高计算任务,查看堆栈跟踪定位代码。
- WAITING/BLOCKED 线程:检查锁竞争或 I/O 等待,定位 synchronized 块或 Lock。
- GC 线程:如果 GC 线程(如 GC task thread)占用高 CPU,可能由于频繁 Full GC 导致。
- 工具:
- FastThread(在线工具):上传线程转储,分析线程状态和热点。
- JProfiler:提供线程分析和 CPU 热点视图。
- Eclipse MAT:结合堆转储分析 GC 问题。
4. 定位代码问题
- 死循环或复杂计算:
- 检查线程堆栈,定位到具体方法(如 while(true) 或递归调用)。
- 使用 jvisualvm 的 Profiler 功能,分析方法调用时间。
- 频繁 GC:
- 检查 GC 日志(启用 -XX:+PrintGCDetails -Xlog:gc*)。
- 如果 Full GC 频繁,可能是内存泄漏或堆大小不足。
- 调整 JVM 参数,如增大堆大小(-Xmx、-Xms)或优化 GC 算法(如使用 G1 或 ZGC)。
- 锁竞争:
- 检查 synchronized 块或 ReentrantLock 使用。
- 使用 jvisualvm 或 JProfiler 分析锁等待时间。
- 优化:减少锁粒度,使用 ConcurrentHashMap 等并发数据结构。
- I/O 瓶颈:
- 检查数据库查询、文件读写或网络调用是否耗时。
- 使用 strace(Linux)跟踪系统调用,定位慢 I/O。
5. 优化和验证
- 优化代码:
- 修复死循环或低效算法。
- 优化数据库查询(如添加索引、减少全表扫描)。
- 使用异步处理或缓存减少 I/O。
- 调整 JVM:
- 优化 GC 配置(如 -XX:+UseG1GC、调整 MaxGCPauseMillis)。
- 调整线程池大小,避免过多线程竞争。
- 验证:
- 部署修复后代码,监控 CPU 使用率。
- 使用压测工具(如 JMeter)验证高负载场景。
三、常用工具总结
工具 | 用途 | 备注 |
---|---|---|
jvisualvm | 监控内存、CPU、线程 | JDK 自带,适合初步分析 |
jmap | 生成堆转储 | 配合 MAT 使用 |
jstack | 生成线程转储 | 分析线程状态 |
Eclipse MAT | 分析堆转储,定位内存泄漏 | 开源,功能强大 |
JProfiler | 内存和 CPU 分析 | 商业工具,界面友好 |
VisualVM | 综合监控和分析 | 支持插件扩展 |
Prometheus + Grafana | 监控内存和 CPU 趋势 | 适合生产环境 |
Arthas | 在线诊断(内存、线程、类加载) | 阿里开源,适合生产环境 |
四、预防措施
- 代码规范:
- 及时关闭资源(如 try-with-resources)。
- 避免静态集合无限增长。
- 使用弱引用或软引用管理缓存。
- 监控和告警:
- 配置监控系统,设置内存和 CPU 使用率告警。
- 定期分析 GC 日志,关注 Full GC 频率。
- 性能测试:
- 在开发阶段进行压力测试,暴露潜在问题。
- 使用工具(如 JMeter、Gatling)模拟高并发场景。
- JVM 优化:
- 根据业务场景选择合适的 GC 算法(如 G1、ZGC)。
- 合理设置堆大小和线程池参数。
五、注意事项
- 生产环境谨慎操作:生成堆转储或线程转储可能导致应用暂停,建议在低峰期或测试环境操作。
- 结合业务场景:内存泄漏和 CPU 高消耗可能与特定业务逻辑相关,需结合日志和代码分析。
- 版本问题:检查 JDK 和第三方库版本,排除已知 Bug。