在生产环境中,Java应用可能会遇到各种性能瓶颈和运行时错误,如CPU占用过高、内存溢出、频繁Full GC或进程意外退出等。这些问题往往相互关联,需要一套系统化的排查方法来快速定位根因并解决。本文将针对几种常见问题,提供详细的排查思路、工具和命令。
问题一:持续出现Full GC
频繁的Full GC会严重拖慢应用性能,导致系统响应变慢甚至无响应。
1. 现象与可能原因
- 现象: 应用响应时间(RT)变长,吞吐量下降,CPU使用率可能因GC线程繁忙而升高。
- 可能原因:
- 内存泄漏: 这是最常见的原因。长生命周期的对象持有了短生命周期对象的引用,导致GC无法回收。
- 堆内存设置过小: 应用实际所需内存超过了JVM堆(Heap)的最大设置(
-Xmx)。 - 大对象过多: 频繁创建大数组或大对象,直接进入老年代,快速填满老年代空间。
- 元空间(Metaspace)不足: 动态加载的类过多(如使用CGLIB、Groovy等),导致元空间被填满,触发Full GC。
- 显式调用: 代码中存在
System.gc()的调用。
2. 排查步骤
第一步:确认GC情况
-
如果已开启GC日志(
-Xloggc:/path/to/gc.log),直接分析日志,观察Full GC的频率、每次回收后的内存占用情况。 -
如果未开启GC日志,可以使用
jstat命令实时监控:bash# 每隔1000毫秒输出一次GC信息,共输出10次 jstat -gcutil <PID> 1000 10- 重点关注
O(老年代使用率)和M(元空间使用率)是否持续很高,以及FGC(Full GC次数)和FGCT(Full GC总耗时)的增长情况。
- 重点关注
第二步:获取堆内存快照(Heap Dump)
当确认是内存问题后,需要获取堆内存快照进行分析。
-
方法A:自动Dump(推荐)
在JVM启动参数中添加以下选项,当发生OOM时会自动生成dump文件。bash-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof -
方法B:手动Dump
在问题发生时,使用jmap命令手动生成。bash# 生成包含所有存活对象的dump文件 jmap -dump:live,format=b,file=/path/to/dump.hprof <PID> -
方法C:从Core文件导出
如果进程已Crash,但有Core文件,可以使用jmap从Core文件中导出堆信息。bashjmap -dump:live,format=b,file=/path/to/dump.hprof $JAVA_HOME/bin/java <core_file_path>
第三步:分析Heap Dump
使用专业的内存分析工具来定位问题。
- Eclipse MAT (Memory Analyzer Tool): 功能强大,免费。
- Histogram: 查看哪些类的实例数量和占用内存最多。
- Dominator Tree: 找出占用内存最多的对象及其引用链。
- Leak Suspects: 工具会自动生成内存泄漏嫌疑报告,通常会直接指向问题代码。
- 其他工具: JProfiler, VisualVM等。
第四步:定位具体代码(进阶)
如果通过Heap Dump只能看到是哪个对象占用内存,但无法定位到具体代码行,可以使用btrace进行动态追踪。
-
原理:
btrace可以在不重启应用的情况下,向JVM中注入探针代码,追踪特定方法的调用。 -
示例: 追踪
com.example.MyService类中processData方法的调用。java// Btrace脚本 @BTrace public class TraceMethod { @OnMethod(clazz = "com.example.MyService", method = "processData") public static void trace(@Self com.example.MyService service, @ProbeClassName String cls, @ProbeMethodName String method) { println("Method called: " + cls + "." + method); jstack(); // 打印调用堆栈 } }
3. 解决方案
- 修复内存泄漏: 根据分析结果,修复代码中不当的对象引用,如清理
static集合、正确使用ThreadLocal并调用remove()、为缓存设置过期时间等。 - 调整JVM参数: 适当增大堆内存(
-Xms,-Xmx)和元空间(-XX:MetaspaceSize,-XX:MaxMetaspaceSize)。 - 优化代码: 避免一次性加载大量数据,改用流式处理或分页查询。
问题二:OOM: unable to create new native thread
此错误并非堆内存溢出,而是操作系统层面的线程资源耗尽。
1. 现象与可能原因
- 现象: 应用无法创建新线程,相关功能(如处理新请求)失败。
- 可能原因:
- 线程数超过系统限制: 达到
ulimit -u(用户最大进程/线程数)或/proc/sys/kernel/threads-max(系统全局线程数上限)。 - 线程泄漏: 线程池配置不当(如无界队列、无最大线程数限制),导致线程不断创建且无法回收。
- 内存不足: 每个线程都需要栈空间(由
-Xss参数控制),如果线程过多,总的虚拟内存可能耗尽。
- 线程数超过系统限制: 达到
2. 排查步骤
第一步:统计线程数
bash
# 统计当前Java进程的线程总数
ps -eLf | grep <PID> | wc -l
第二步:检查系统限制
bash
# 查看当前用户的线程数限制
ulimit -u
# 查看系统全局限制
cat /proc/sys/kernel/threads-max
# 查看当前进程的详细限制
cat /proc/<PID>/limits | grep "Max processes"
第三步:分析线程堆栈
使用jstack分析线程状态,查看线程都在做什么。
bash
jstack -l <PID> > thread_dump.log
- 在
thread_dump.log中,统计BLOCKED、WAITING、TIMED_WAITING状态的线程数量。如果发现大量线程阻塞或等待,说明存在严重的锁竞争或资源等待问题。
3. 解决方案
- 调整系统限制: 如果线程数未达应用预期但达到系统限制,可适当调大
ulimit -u的值。 - 优化线程池: 合理设置线程池的核心线程数、最大线程数和队列大小,避免无限制创建线程。
- 减小线程栈大小: 如果线程数确实很多且内存紧张,可以尝试减小
-Xss参数,例如从默认的1M减小到256k或512k。
问题三:OOM: java heap space
这是最典型的堆内存溢出错误。
1. 现象与可能原因
- 现象: 应用抛出
java.lang.OutOfMemoryError: Java heap space异常并可能崩溃。 - 可能原因: 与"持续Full GC"的原因高度重合,主要是内存泄漏或堆空间不足。
2. 排查步骤
排查方法与"持续Full GC"基本一致,核心是获取并分析Heap Dump。
- 关键一步: 务必在JVM启动时加上
-XX:+HeapDumpOnOutOfMemoryError,这是定位问题的"黑匣子"。 - 分析工具: 使用MAT分析dump文件,找到占用内存最多的对象和它们的引用链(GC Roots)。
3. 解决方案
同"持续Full GC"的解决方案。
问题四:Java进程意外退出
进程在没有明显异常日志的情况下突然消失。
1. 现象与可能原因
- 现象: 进程ID(PID)不存在,应用服务中断。
- 可能原因:
- 被操作系统杀死(OOM Killer): 系统物理内存不足,内核为了保护系统稳定,会选择一个进程杀死。
- 代码Bug导致Crash: 如JNI本地代码错误、无限递归导致栈溢出等。
- 人为操作: 被
kill -9等命令强制终止。
2. 排查步骤
第一步:检查系统日志
这是最重要的一步,查看内核日志,确认进程是否被OOM Killer杀死。
bash
dmesg | grep -i "killed process"
dmesg | grep -i "out of memory"
# 或者直接查看系统日志文件
tail -n 100 /var/log/messages
如果看到类似Out of memory: Kill process <PID> (java)的记录,则说明是内存不足导致。
第二步:生成并分析Core Dump
为了定位代码级Crash,需要启用Core Dump。
- 启用Core Dump: 在应用启动脚本中设置
ulimit -c unlimited。 - 查找Core文件: 进程Crash后,Core文件通常生成在应用的工作目录下,文件名可能是
core或core.<PID>。 - 分析Core文件:
-
Java层: 使用
jstack分析Java线程堆栈,看是否有死循环或无限递归。bashjstack $JAVA_HOME/bin/java core.<PID> > jstack_from_core.log -
Native层: 使用
gdb等调试器分析,定位是否是JNI代码或JVM本身的Bug。
-
3. 解决方案
- 内存不足: 增加机器内存,或优化应用内存使用,或调整容器的内存限制。
- 代码Bug: 根据堆栈信息修复代码。
问题五:CPU占用过高
CPU飙高会抢占系统资源,影响其他服务。
1. 现象与可能原因
- 现象:
top命令显示Java进程的%CPU值非常高。 - 可能原因:
- 频繁GC: 尤其是Full GC,会消耗大量CPU。
- 代码死循环: 存在未正确退出的
while循环或递归。 - 复杂计算: 如复杂的正则表达式(回溯爆炸)、序列化/反序列化、大数据量排序等。
- 锁竞争: 大量线程竞争同一把锁,导致上下文切换频繁,内核态CPU(
sy)升高。
2. 排查步骤
第一步:区分CPU占用类型
使用top命令,观察us(用户态)、sy(内核态)、wa(IO等待)的占比。
us高: 通常是应用代码问题(死循环、复杂计算)或频繁GC。sy高: 通常是线程过多、频繁上下文切换或锁竞争。wa高: 通常是磁盘IO瓶颈,不是CPU问题。
第二步:排除GC问题
使用jstat -gcutil <PID> 1000观察GC情况,如果FGC非常频繁,则问题根源在内存。
第三步:定位高CPU线程
如果不是GC问题,则定位具体是哪个线程消耗了CPU。
bash
# 1. 以线程为单位查看CPU占用
top -Hp <PID>
# 2. 找到CPU占用最高的线程ID(例如是12345)
# 3. 将线程ID转换为16进制
printf "%x\n" 12345
# 输出: 3039
# 4. 在jstack日志中查找对应的线程
jstack <PID> | grep -A 20 "nid=0x3039"
通过分析该线程的堆栈,就能知道它在执行什么代码。例如,堆栈可能指向一个正则表达式的match方法,或者一个HashMap的get操作(在并发场景下可能导致死循环)。
3. 解决方案
- 频繁GC: 按内存问题处理。
- 死循环/复杂计算: 优化代码逻辑,修复死循环,优化算法复杂度。
- 锁竞争: 优化锁的粒度,或使用无锁数据结构。