JAVA 项目 CPU 飙升定位?使用 jstack 分析死锁与阻塞线程

JAVA 项目 CPU 飙升定位:jstack 分析死锁与阻塞线程

大家好,今天我们来聊聊 Java 项目 CPU 飙升的定位与排查,重点讲解如何使用 jstack 命令分析死锁和阻塞线程,从而找到性能瓶颈。CPU 飙升是线上问题中比较常见的一种,原因多种多样,但线程问题往往是罪魁祸首之一。

一、CPU 飙升的常见原因

在深入分析之前,我们先简单了解一下导致 CPU 飙升的常见原因,以便缩小问题范围:

  1. 死循环/无限递归: 代码逻辑错误导致程序陷入无限循环,持续占用 CPU 资源。
  2. 频繁的 GC: 大量对象创建导致垃圾回收器频繁工作,占用 CPU 时间。
  3. 不合理的线程模型: 创建过多线程,线程上下文切换消耗大量 CPU 资源。
  4. IO 密集型操作: 大量读写磁盘或网络操作阻塞线程,导致 CPU 空转。
  5. 死锁/锁竞争: 多个线程竞争同一资源,导致线程阻塞,CPU 利用率下降。但如果有很多线程都在等待锁,竞争非常激烈,也会导致 CPU 飙升。
  6. 大量计算:复杂的算法或者大量计算操作,会消耗大量的CPU资源。
  7. 正则表达式问题: 复杂的正则表达式匹配可能导致回溯,消耗大量 CPU 资源。

二、定位 CPU 飙升的基本步骤

  1. 监控 CPU 使用率: 使用 tophtop 等系统工具监控 CPU 使用率,确认是否真的存在 CPU 飙升。

  2. 找到高 CPU 进程 ID: 使用 top 命令找到占用 CPU 最高的 Java 进程 ID (PID)。

  3. 确定高 CPU 线程 ID: 使用 top -H -p PID 命令(PID 替换为实际的进程 ID)找到占用 CPU 最高的线程 ID。 请注意,这里显示的线程 ID 是十进制的。

  4. 将线程 ID 转换为十六进制: 使用 printf "%xn" TID 命令(TID 替换为十进制的线程 ID)将线程 ID 转换为十六进制,因为 jstack 输出的线程 ID 是十六进制的。

  5. 使用 jstack 命令导出线程栈信息: 使用 jstack PID > jstack.log 命令导出 Java 进程的线程栈信息到 jstack.log 文件中。

  6. 分析 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();
    }
}

分析步骤:

  1. 编译并运行上述代码。

  2. 使用 jps 命令找到进程 ID。

  3. 使用 jstack -l PID > jstack.log 命令导出线程栈信息。

  4. 打开 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),而 lock2Thread-0 持有。反过来,Thread-0 正在等待 lock1 (0x000000076b5f69d8),而 lock1Thread-1 持有。这就形成了一个典型的死锁环。同时,jstack 还给出了每个线程的堆栈信息,可以精确定位到死锁发生的代码位置 (DeadlockExample.java:15 和 DeadlockExample.java:26)。

解决死锁:

解决死锁的常见方法包括:

  • 避免多个锁: 尽量减少线程需要同时持有的锁的数量。
  • 锁的顺序一致: 确保所有线程以相同的顺序获取锁。
  • 使用超时机制: 在获取锁时设置超时时间,避免线程永久等待。
  • 使用死锁检测工具: 使用专门的死锁检测工具,例如 jconsoleVisualVM 等。

五、使用 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();
    }
}

分析步骤:

  1. 编译并运行上述代码。

  2. 使用 jps 命令找到进程 ID。

  3. 使用 jstack -l PID > jstack.log 命令导出线程栈信息。

  4. 打开 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 输出的线程状态是理解线程行为的关键。常见的线程状态包括:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

  • 结合其他工具: jstack 只是一个线程堆栈分析工具,需要结合其他工具(例如 jstatjconsoleVisualVM)才能更全面地了解 Java 程序的性能状况。

八、使用arthas 诊断工具

Arthas 是一款优秀的 Java 诊断工具,它提供了比 jstack 更丰富的功能,例如:

  • thread 命令: 可以查看所有线程的状态、CPU 使用率、阻塞时间等信息,还可以过滤指定状态的线程。
  • stack 命令: 可以打印指定线程的堆栈信息,类似于 jstack
  • monitor 命令: 可以监控指定方法的调用情况,例如调用次数、平均耗时、错误率等。
  • trace 命令: 可以追踪指定方法的调用链路,帮助你了解方法的执行过程。
  • watch 命令: 可以观察指定方法的参数、返回值、异常等信息。

使用 Arthas 可以更方便地定位 CPU 飙升的原因,并且可以动态地修改代码,无需重启应用。

总结

本文详细介绍了如何使用 jstack 命令分析 Java 项目 CPU 飙升问题,重点讲解了如何定位死锁和阻塞线程。同时,也介绍了 Arthas 这款更强大的 Java 诊断工具。掌握这些工具和技巧,可以帮助你快速定位和解决 Java 程序的性能问题。

使用jstack分析死锁和阻塞线程是解决CPU飙升问题的常用方法。定位问题时,需要结合实际情况,灵活运用各种工具和技巧,才能找到问题的根源。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注