JAVA 项目 CPU 飙升定位:jstack 分析死锁与阻塞线程
大家好,今天我们来聊聊 Java 项目 CPU 飙升的定位与排查,重点讲解如何使用 jstack 命令分析死锁和阻塞线程,从而找到性能瓶颈。CPU 飙升是线上问题中比较常见的一种,原因多种多样,但线程问题往往是罪魁祸首之一。
一、CPU 飙升的常见原因
在深入分析之前,我们先简单了解一下导致 CPU 飙升的常见原因,以便缩小问题范围:
- 死循环/无限递归: 代码逻辑错误导致程序陷入无限循环,持续占用 CPU 资源。
- 频繁的 GC: 大量对象创建导致垃圾回收器频繁工作,占用 CPU 时间。
- 不合理的线程模型: 创建过多线程,线程上下文切换消耗大量 CPU 资源。
- IO 密集型操作: 大量读写磁盘或网络操作阻塞线程,导致 CPU 空转。
- 死锁/锁竞争: 多个线程竞争同一资源,导致线程阻塞,CPU 利用率下降。但如果有很多线程都在等待锁,竞争非常激烈,也会导致 CPU 飙升。
- 大量计算:复杂的算法或者大量计算操作,会消耗大量的CPU资源。
- 正则表达式问题: 复杂的正则表达式匹配可能导致回溯,消耗大量 CPU 资源。
二、定位 CPU 飙升的基本步骤
-
监控 CPU 使用率: 使用
top、htop等系统工具监控 CPU 使用率,确认是否真的存在 CPU 飙升。 -
找到高 CPU 进程 ID: 使用
top命令找到占用 CPU 最高的 Java 进程 ID (PID)。 -
确定高 CPU 线程 ID: 使用
top -H -p PID命令(PID 替换为实际的进程 ID)找到占用 CPU 最高的线程 ID。 请注意,这里显示的线程 ID 是十进制的。 -
将线程 ID 转换为十六进制: 使用
printf "%xn" TID命令(TID 替换为十进制的线程 ID)将线程 ID 转换为十六进制,因为jstack输出的线程 ID 是十六进制的。 -
使用
jstack命令导出线程栈信息: 使用jstack PID > jstack.log命令导出 Java 进程的线程栈信息到jstack.log文件中。 -
分析
jstack.log文件: 分析jstack.log文件,查找死锁、阻塞等信息。
三、jstack 命令详解
jstack 是 JDK 自带的线程堆栈分析工具,可以用于查看指定 Java 进程的线程堆栈信息。其基本语法如下:
jstack [options] pid
常用的 options 包括:
-l:显示锁的附加信息,例如拥有者、等待者等。强烈建议加上这个参数。-m:显示 native 方法栈帧信息。-F:当jstack命令无响应时,强制导出线程栈信息。谨慎使用。
四、使用 jstack 分析死锁
死锁是指两个或多个线程互相持有对方需要的资源,导致所有线程都无法继续执行的状态。jstack 可以检测到死锁,并在输出中明确标识。
示例代码:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock1...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock2.");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock2...");
try { Thread.sleep(10); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock1.");
}
}
});
thread1.start();
thread2.start();
}
}
分析步骤:
-
编译并运行上述代码。
-
使用
jps命令找到进程 ID。 -
使用
jstack -l PID > jstack.log命令导出线程栈信息。 -
打开
jstack.log文件,搜索 "deadlock"。
jstack.log 文件中死锁信息的示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f9c8408b000 (object 0x000000076b5f6a08, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f9c8408a000 (object 0x000000076b5f69d8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockExample.lambda$main$1(DeadlockExample.java:26)
- waiting to lock <0x000000076b5f6a08> (a java.lang.Object)
- locked <0x000000076b5f69d8> (a java.lang.Object)
at DeadlockExample$$Lambda$2/0x0000000800c00840.run(Unknown Source)
at java.lang.Thread.run([email protected]/Thread.java:829)
"Thread-0":
at DeadlockExample.lambda$main$0(DeadlockExample.java:15)
- waiting to lock <0x000000076b5f69d8> (a java.lang.Object)
- locked <0x000000076b5f6a08> (a java.lang.Object)
at DeadlockExample$$Lambda$1/0x0000000800c00640.run(Unknown Source)
at java.lang.Thread.run([email protected]/Thread.java:829)
Found 1 deadlock.
分析结果:
jstack 明确指出了存在一个 Java 级别的死锁。Thread-1 正在等待 lock2 (0x000000076b5f6a08),而 lock2 被 Thread-0 持有。反过来,Thread-0 正在等待 lock1 (0x000000076b5f69d8),而 lock1 被 Thread-1 持有。这就形成了一个典型的死锁环。同时,jstack 还给出了每个线程的堆栈信息,可以精确定位到死锁发生的代码位置 (DeadlockExample.java:15 和 DeadlockExample.java:26)。
解决死锁:
解决死锁的常见方法包括:
- 避免多个锁: 尽量减少线程需要同时持有的锁的数量。
- 锁的顺序一致: 确保所有线程以相同的顺序获取锁。
- 使用超时机制: 在获取锁时设置超时时间,避免线程永久等待。
- 使用死锁检测工具: 使用专门的死锁检测工具,例如
jconsole、VisualVM等。
五、使用 jstack 分析阻塞线程
阻塞是指线程因为等待某个资源(例如锁、IO)而暂停执行的状态。大量的阻塞线程会导致 CPU 利用率下降,甚至导致系统响应缓慢。
示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BlockedThreadExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread 1: Acquired lock.");
Thread.sleep(10000); // 模拟长时间持有锁
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Thread 1: Releasing lock.");
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2: Trying to acquire lock...");
lock.lock(); // 线程2 将在此处阻塞
try {
System.out.println("Thread 2: Acquired lock.");
} finally {
System.out.println("Thread 2: Releasing lock.");
lock.unlock();
}
});
thread1.start();
Thread.sleep(100); // 确保线程1 先获取锁
thread2.start();
}
}
分析步骤:
-
编译并运行上述代码。
-
使用
jps命令找到进程 ID。 -
使用
jstack -l PID > jstack.log命令导出线程栈信息。 -
打开
jstack.log文件,查找 "waiting on condition" 或 "blocked" 状态的线程。
jstack.log 文件中阻塞线程信息的示例:
"Thread-1":
java.lang.Thread.State: BLOCKED (on object monitor)
at BlockedThreadExample.lambda$main$1(BlockedThreadExample.java:30)
- waiting to lock <0x000000076b606318> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at BlockedThreadExample$$Lambda$2/0x0000000800c00840.run(Unknown Source)
at java.lang.Thread.run([email protected]/Thread.java:829)
"Thread-0":
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep([email protected]/Native Method)
at BlockedThreadExample.lambda$main$0(BlockedThreadExample.java:19)
- locked <0x000000076b606318> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
at BlockedThreadExample$$Lambda$1/0x0000000800c00640.run(Unknown Source)
at java.lang.Thread.run([email protected]/Thread.java:829)
分析结果:
jstack 显示 Thread-1 的状态是 BLOCKED,并且正在等待获取 lock (0x000000076b606318)。Thread-0 的状态是 TIMED_WAITING,因为它正在 sleep,并且持有 lock (0x000000076b606318)。这表明 Thread-1 因为等待 Thread-0 释放锁而被阻塞。
解决阻塞线程:
解决阻塞线程的常见方法包括:
- 缩短锁的持有时间: 尽量减少线程持有锁的时间,避免长时间阻塞其他线程。
- 使用非阻塞算法: 考虑使用非阻塞算法(例如 CAS)来避免锁竞争。
- 优化 IO 操作: 减少 IO 操作的次数和时间,避免线程长时间阻塞在 IO 上。
- 增加资源: 如果阻塞是因为资源不足,可以考虑增加资源,例如增加数据库连接池大小。
- 公平锁:如果使用的是可重入锁,可以考虑使用公平锁,避免某些线程饥饿。
六、jstack 分析的其他技巧
-
结合 CPU 使用率: 将
jstack的分析结果与 CPU 使用率结合起来,可以更准确地定位性能瓶颈。例如,如果 CPU 使用率很高,并且jstack发现大量线程处于RUNNABLE状态,则可能是代码存在计算密集型操作。 -
多次
jstack: 多次执行jstack命令,并比较输出结果,可以观察线程状态的变化,从而更好地理解线程的行为。 -
关注 WAITING 和 TIMED_WAITING 状态: 除了 BLOCKED 状态,WAITING 和 TIMED_WAITING 状态也可能表示线程正在等待某些资源,需要进一步分析。 例如,WAITING 状态可能表示线程正在等待
Object.wait()或LockSupport.park()的唤醒,TIMED_WAITING 状态可能表示线程正在等待Thread.sleep()或LockSupport.parkNanos()的结束。 -
注意线程优先级:
jstack可以显示线程的优先级,高优先级的线程可能会抢占低优先级线程的 CPU 资源。 -
分析线程名称: 给线程起一个有意义的名字,可以更容易地在
jstack输出中找到目标线程。
七、常见问题和注意事项
-
jstack命令无响应: 如果jstack命令无响应,可以使用-F选项强制导出线程栈信息。但请注意,这可能会导致 JVM 崩溃,因此谨慎使用。 通常无响应是因为目标进程已经处于非常繁忙的状态,尝试增加 jstack 的超时时间可能会有所帮助。 -
jstack输出信息过多:jstack的输出信息可能非常多,可以使用文本编辑器或命令行工具(例如grep)来过滤和查找关键信息。 -
线上环境的安全性: 在生产环境中使用
jstack命令时,需要注意安全性,避免泄露敏感信息。 -
理解线程状态:
jstack输出的线程状态是理解线程行为的关键。常见的线程状态包括:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。 -
结合其他工具:
jstack只是一个线程堆栈分析工具,需要结合其他工具(例如jstat、jconsole、VisualVM)才能更全面地了解 Java 程序的性能状况。
八、使用arthas 诊断工具
Arthas 是一款优秀的 Java 诊断工具,它提供了比 jstack 更丰富的功能,例如:
- thread 命令: 可以查看所有线程的状态、CPU 使用率、阻塞时间等信息,还可以过滤指定状态的线程。
- stack 命令: 可以打印指定线程的堆栈信息,类似于
jstack。 - monitor 命令: 可以监控指定方法的调用情况,例如调用次数、平均耗时、错误率等。
- trace 命令: 可以追踪指定方法的调用链路,帮助你了解方法的执行过程。
- watch 命令: 可以观察指定方法的参数、返回值、异常等信息。
使用 Arthas 可以更方便地定位 CPU 飙升的原因,并且可以动态地修改代码,无需重启应用。
总结
本文详细介绍了如何使用 jstack 命令分析 Java 项目 CPU 飙升问题,重点讲解了如何定位死锁和阻塞线程。同时,也介绍了 Arthas 这款更强大的 Java 诊断工具。掌握这些工具和技巧,可以帮助你快速定位和解决 Java 程序的性能问题。
使用jstack分析死锁和阻塞线程是解决CPU飙升问题的常用方法。定位问题时,需要结合实际情况,灵活运用各种工具和技巧,才能找到问题的根源。