JAVA CPU 占用高的线程如何快速定位?JStack + Arthas 实战演示
大家好,今天我们来聊聊Java应用CPU占用率高的问题,以及如何快速定位导致问题的线程。CPU占用率高是线上问题中比较常见的一种,它可能导致应用响应变慢,甚至崩溃。快速定位问题并解决,对于保证系统稳定至关重要。
我们将以JStack和Arthas两个工具为例,结合实际案例,深入探讨如何排查CPU占用率高的线程。
一、CPU占用率高的常见原因
在开始排查之前,我们先了解下导致CPU占用率高的常见原因:
- 死循环或无限循环: 线程陷入循环,无法正常退出,持续消耗CPU资源。
- 频繁的GC(垃圾回收): 当堆内存不足时,JVM会频繁进行GC,导致CPU占用率升高。
- 大量的I/O操作: 频繁的磁盘I/O或网络I/O会导致线程阻塞,CPU空转。
- 复杂的计算: 线程执行大量的计算密集型任务,例如图像处理、音视频编解码等。
- 锁竞争激烈: 多个线程竞争同一把锁,导致大量线程阻塞,CPU在线程上下文切换上消耗。
- 正则表达式效率低下: 使用不当的正则表达式进行匹配,可能导致回溯,消耗大量CPU资源。
二、JStack:初步定位
JStack是JDK自带的线程堆栈分析工具,它可以dump出JVM中所有线程的堆栈信息。通过分析线程堆栈,我们可以初步定位到CPU占用率高的线程。
1. 获取进程ID (PID)
首先,我们需要获取Java进程的PID。可以使用jps命令或者ps命令来获取。
jps -lv
# 或者
ps -ef | grep java
2. 使用JStack生成线程堆栈信息
获取到PID后,就可以使用jstack命令生成线程堆栈信息。
jstack <pid> > stack.log
将线程堆栈信息保存到stack.log文件中。
3. 分析线程堆栈信息
打开stack.log文件,我们需要关注以下几个方面:
- 线程状态: 重点关注状态为
RUNNABLE的线程,这些线程正在运行,可能是导致CPU占用率高的罪魁祸首。 - 线程优先级: 优先级高的线程更有可能占用更多的CPU资源。
- 线程堆栈信息: 仔细阅读线程的堆栈信息,找到线程正在执行的代码。
案例分析:死循环
假设stack.log文件中有以下堆栈信息:
"Thread-1" #15 prio=5 os_prio=0 tid=0x00007f80d0c00000 nid=0x1003 runnable [0x000070000f500000]
java.lang.Thread.State: RUNNABLE
at com.example.demo.LoopExample.run(LoopExample.java:10)
at java.lang.Thread.run(Thread.java:748)
可以看到,线程Thread-1的状态为RUNNABLE,并且正在执行com.example.demo.LoopExample.run()方法。查看LoopExample.java的代码:
package com.example.demo;
public class LoopExample implements Runnable {
@Override
public void run() {
while (true) {
// 死循环
}
}
}
这段代码就是一个典型的死循环,导致线程一直运行,占用CPU资源。
案例分析:锁竞争
假设stack.log文件中有以下堆栈信息:
"Thread-2" #16 prio=5 os_prio=0 tid=0x00007f80d0c01000 nid=0x1004 waiting for monitor entry [0x000070000f600000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.demo.LockExample.increase(LockExample.java:15)
- waiting to lock <0x000000076b044670> (a com.example.demo.LockExample)
at com.example.demo.LockExample.run(LockExample.java:20)
at java.lang.Thread.run(Thread.java:748)
"Thread-3" #17 prio=5 os_prio=0 tid=0x00007f80d0c02000 nid=0x1005 runnable [0x000070000f700000]
java.lang.Thread.State: RUNNABLE
at com.example.demo.LockExample.increase(LockExample.java:15)
- locked <0x000000076b044670> (a com.example.demo.LockExample)
at com.example.demo.LockExample.run(LockExample.java:20)
at java.lang.Thread.run(Thread.java:748)
可以看到,线程Thread-2的状态为BLOCKED,正在等待获取对象<0x000000076b044670>的锁。而线程Thread-3的状态为RUNNABLE,并且已经获得了该锁。这说明存在锁竞争,导致线程Thread-2阻塞,而CPU可能在线程上下文切换上消耗。查看LockExample.java的代码:
package com.example.demo;
public class LockExample implements Runnable {
private int count = 0;
private final Object lock = new Object();
public void increase() {
synchronized (lock) {
count++;
}
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
increase();
}
}
}
这段代码使用了synchronized关键字对increase()方法进行同步,多个线程同时调用increase()方法时,会竞争lock对象,导致锁竞争。
JStack的局限性
JStack虽然可以帮助我们初步定位问题,但是它也有一些局限性:
- 静态分析: JStack只能生成某一时刻的线程堆栈信息,无法动态地跟踪线程的运行状态。
- 信息有限: JStack提供的线程堆栈信息比较有限,无法提供更详细的上下文信息,例如方法参数、局部变量等。
- 定位困难: 当线程堆栈信息比较复杂时,很难快速定位到导致CPU占用率高的线程。
三、Arthas:动态诊断
Arthas是一款功能强大的Java诊断工具,它可以在不重启应用的情况下,动态地诊断Java应用的问题。相比于JStack,Arthas提供了更多的功能,可以更方便地定位CPU占用率高的线程。
1. 安装和启动Arthas
下载Arthas:
wget https://arthas.aliyun.com/arthas-boot.jar
启动Arthas:
java -jar arthas-boot.jar
选择要诊断的Java进程。
2. 使用thread命令
Arthas的thread命令可以查看当前JVM中所有线程的信息,包括线程ID、线程名称、线程状态、CPU占用率等。
thread
3. 找出CPU占用率最高的线程
使用thread -n <count>命令可以找出CPU占用率最高的几个线程。例如,找出CPU占用率最高的3个线程:
thread -n 3
4. 查看线程堆栈信息
使用thread <thread-id>命令可以查看指定线程的堆栈信息。例如,查看线程ID为15的线程的堆栈信息:
thread 15
5. 使用trace命令
Arthas的trace命令可以跟踪方法的执行路径,并统计方法的执行时间。通过trace命令,我们可以找到执行时间最长的方法,从而定位到导致CPU占用率高的代码。
trace com.example.demo.LoopExample run
这条命令会跟踪com.example.demo.LoopExample.run()方法的执行路径,并统计方法的执行时间。
6. 使用watch命令
Arthas的watch命令可以观察方法的参数、返回值和异常。通过watch命令,我们可以观察方法的输入输出,从而更好地理解方法的行为,定位问题。
watch com.example.demo.LockExample increase "{params,returnObj,throwExp}" -n 5
这条命令会观察com.example.demo.LockExample.increase()方法的参数、返回值和异常,并输出前5次调用的结果。
案例分析:正则表达式
假设应用中使用正则表达式进行匹配,但是由于正则表达式效率低下,导致CPU占用率升高。
可以使用trace命令来跟踪正则表达式的匹配过程:
trace java.util.regex.Pattern compile
这条命令会跟踪java.util.regex.Pattern.compile()方法的执行路径,并统计方法的执行时间。如果发现compile方法的执行时间很长,说明正则表达式的编译过程比较耗时。
可以使用watch命令来观察正则表达式的匹配过程:
watch java.util.regex.Matcher matches "{params,returnObj,throwExp}" -n 5
这条命令会观察java.util.regex.Matcher.matches()方法的参数、返回值和异常,并输出前5次调用的结果。通过观察params,我们可以找到导致CPU占用率高的正则表达式。
四、JStack和Arthas结合使用
JStack和Arthas可以结合使用,更好地定位CPU占用率高的线程。
- 使用JStack初步定位: 首先使用JStack生成线程堆栈信息,找到状态为
RUNNABLE的线程,初步判断可能存在问题的代码。 - 使用Arthas深入分析: 然后使用Arthas的
thread命令找到CPU占用率最高的线程,并使用trace和watch命令深入分析这些线程的执行路径和方法调用,找到导致CPU占用率高的具体代码。
五、实战案例
我们来模拟一个实际的案例,演示如何使用JStack和Arthas定位CPU占用率高的线程。
1. 模拟CPU占用率高的场景
package com.example.demo;
import java.util.Random;
public class CPUDemo {
public static void main(String[] args) throws InterruptedException {
// 模拟CPU占用率高的线程
new Thread(() -> {
Random random = new Random();
while (true) {
// 复杂的计算
double result = Math.pow(random.nextDouble(), random.nextDouble());
}
}).start();
// 模拟I/O操作
new Thread(() -> {
while (true) {
try {
Thread.sleep(10);
System.out.println("I/O operation");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(Long.MAX_VALUE);
}
}
这段代码创建了两个线程:
- 第一个线程进行复杂的计算,模拟CPU占用率高的场景。
- 第二个线程进行I/O操作,模拟I/O阻塞的场景。
2. 使用JStack定位
首先,使用jps命令找到CPUDemo进程的PID。
jps -lv
假设PID为12345。
然后,使用jstack命令生成线程堆栈信息。
jstack 12345 > stack.log
打开stack.log文件,找到状态为RUNNABLE的线程。
"Thread-0" #10 prio=5 os_prio=0 tid=0x00007f80d0c00000 nid=0x1003 runnable [0x000070000f500000]
java.lang.Thread.State: RUNNABLE
at java.lang.Math.pow(Native Method)
at com.example.demo.CPUDemo.lambda$0(CPUDemo.java:10)
at com.example.demo.CPUDemo$$Lambda$1/1740447996.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
可以看到,线程Thread-0的状态为RUNNABLE,并且正在执行java.lang.Math.pow()方法和com.example.demo.CPUDemo.lambda$0()方法。这说明该线程可能导致CPU占用率高。
3. 使用Arthas定位
启动Arthas,并选择CPUDemo进程。
使用thread命令找出CPU占用率最高的线程。
thread -n 3
假设输出结果如下:
"Thread-0" Id=10 cpuUsage=99% deltaCpuUsage=1% RUNNABLE
at java.lang.Math.pow(Native Method)
at com.example.demo.CPUDemo.lambda$0(CPUDemo.java:10)
at com.example.demo.CPUDemo$$Lambda$1/1740447996.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
...
可以看到,线程Thread-0的CPU占用率为99%,确认了该线程导致CPU占用率高。
使用trace命令跟踪java.lang.Math.pow()方法的执行路径。
trace java.lang.Math pow
通过trace命令的输出结果,可以确认java.lang.Math.pow()方法被频繁调用,并且执行时间很长,导致CPU占用率高。
六、解决问题
通过上述分析,我们已经定位到导致CPU占用率高的线程和代码。接下来,我们需要解决问题。
- 死循环: 修复死循环,确保线程可以正常退出。
- 锁竞争: 减少锁的持有时间,使用更细粒度的锁,或者使用无锁数据结构。
- 复杂的计算: 优化算法,减少计算量,或者使用缓存。
- 正则表达式: 优化正则表达式,避免回溯。
在本案例中,我们可以修改代码,降低计算的频率,或者使用缓存来避免重复计算。
优化后的代码
package com.example.demo;
import java.util.Random;
public class CPUDemo {
public static void main(String[] args) throws InterruptedException {
// 模拟CPU占用率高的线程
new Thread(() -> {
Random random = new Random();
while (true) {
// 复杂的计算,降低计算频率
if (random.nextInt(100) == 0) {
double result = Math.pow(random.nextDouble(), random.nextDouble());
}
try {
Thread.sleep(1); // 降低CPU占用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 模拟I/O操作
new Thread(() -> {
while (true) {
try {
Thread.sleep(10);
System.out.println("I/O operation");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(Long.MAX_VALUE);
}
}
七、一些额外的技巧
- 查看系统资源: 使用
top命令或htop命令查看系统的CPU、内存、I/O等资源的使用情况,可以帮助我们判断CPU占用率高是由哪些原因引起的。 - 使用性能分析工具: 除了JStack和Arthas,还可以使用一些专业的性能分析工具,例如JProfiler、YourKit等,这些工具可以提供更详细的性能分析报告。
- 监控系统指标: 建立完善的监控系统,监控CPU占用率、内存使用率、GC时间等关键指标,可以帮助我们及时发现问题。
关键步骤的回顾
使用JStack和Arthas结合的方式能够有效地定位CPU占用高的问题。首先利用JStack拿到线程栈信息,然后使用Arthas的thread命令精确定位CPU占用率最高的线程,再使用trace和watch命令分析其执行路径和方法调用,最终找到问题的根源。
一些建议
遇到CPU占用率高的问题时,不要慌张,首先冷静分析,然后选择合适的工具进行排查。JStack和Arthas都是非常强大的工具,熟练掌握它们,可以帮助我们快速定位并解决问题。记住,监控和预防胜于治疗,建立完善的监控系统,可以帮助我们及时发现问题,避免损失。