JAVA CPU 占用高的线程如何快速定位?JStack + Arthas 实战演示

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占用率高的线程。

  1. 使用JStack初步定位: 首先使用JStack生成线程堆栈信息,找到状态为RUNNABLE的线程,初步判断可能存在问题的代码。
  2. 使用Arthas深入分析: 然后使用Arthas的thread命令找到CPU占用率最高的线程,并使用tracewatch命令深入分析这些线程的执行路径和方法调用,找到导致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占用率最高的线程,再使用tracewatch命令分析其执行路径和方法调用,最终找到问题的根源。

一些建议

遇到CPU占用率高的问题时,不要慌张,首先冷静分析,然后选择合适的工具进行排查。JStack和Arthas都是非常强大的工具,熟练掌握它们,可以帮助我们快速定位并解决问题。记住,监控和预防胜于治疗,建立完善的监控系统,可以帮助我们及时发现问题,避免损失。

发表回复

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