JVM系列(三十四) JVM调优实战-Jstack CPU过高问题分析及定位

496 阅读5分钟

Jstack CPU过高问题分析及定位

前面我们讲解了Jstack命令可以排查死锁问题,通过jstack日志,我们看到了线程之间相互持有,造成死锁,并且能够顺利的定位到死锁,看到代码的位置解决死锁问题,死锁是造成系统停顿时间久的问题之一,但是系统停顿的问题有很多种,今天我们分析下CPU过高的问题定位

学习了这篇文章后,在项目生产环境上,如果遇到CPU问题很高,让我们排查下原因,就不会无从下手了

1.CPU过高问题分析

真正的项目中,CPU过高时,尽量让运维配置下脚本,在CPU超过多少的时候就自动打印线程堆栈信息,有了堆栈信息我们才能分析那时刻,到底在干什么,什么导致CPU过高

下面,我们编写一个程序,来模拟一下CPU问题过高,然后通过Jstack命令,快速定位到CPU过高的问题,解决问题

场景如下:

  • 开启2个线程,每个线程里面都在循环运算
  • 运算就会导致CPU升高,这时候检测下CPU的信息
  • CPU升高后,我们执行Jstack命令,打印线程堆栈信息
  • 然后定位问题,解决问题

本次环境我们要执行一些Linux 命令,windows 环境不行,所以我们这次在虚拟机中执行本次操作,需要把程序打包,然后放到linux服务器上执行,然后执行命令 定位问题, 步骤如下

top
top -Hp pid
printf %x xxx jstack -F -l pid
jstack -l [PID] >/tmp/log.txt
分析堆栈信息

1.1 程序代码及打包install

依旧是通过 TestController启动运行计算程序,代码如下

package com.jzj.jvmtest.font;

import com.jzj.jvmtest.mythread.MyTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@Slf4j
public class TestController {

    /**
     * 新建一个固定大小的线程池
     */
    private static ExecutorService executorService = Executors.newFixedThreadPool(5);


    public static Object TEST_LOCK = new Object();


    @GetMapping("/ping")
    private String ping() {
        return "pong";
    }

  
    @GetMapping("/cpu")
    private String cpu() {
        MyTask task1 = new MyTask();
        MyTask task2 = new MyTask();
        executorService.execute(task1);
        executorService.execute(task2);
        return "ok";
    }
}

然后这里面有两个MyTask 线程信息,代码如下

package com.jzj.jvmtest.mythread;

import static com.jzj.jvmtest.font.TestController.TEST_LOCK;

public class MyTask implements Runnable {

    public void run() {
        synchronized (TEST_LOCK) {
            //线程开始计算sum, 累加,消耗CPU
            long sum = 0L;
            while (true) {
                sum += 1;
            }
        }
    }
}

通过线程池 来开启线程

MyTask task1 = new MyTask();
MyTask task2 = new MyTask();
executorService.execute(task1);
executorService.execute(task2);

SpringBoot启动后,我们通过 curl 127.0.0.1:8078/temp/cpu 来执行程序,让cpu跑起来

1.2 复制jar包到linux, 执行

我们把Jar包复制到Linux 服务器,然后执行Jar包

image.png

Linux环境下 启动Jar包

java -jar demo-0.0.1-SNAPSHOT.jar

[jzj@localhost jzjtest]$ pwd
/home/jzj/jzjtest
[jzj@localhost jzjtest]$ ll
总用量 18856
-rwxrw-rw-. 1 jzj jzj 19308165 426 2023 demo-0.0.1-SNAPSHOT.jar
[jzj@localhost jzjtest]$ 
[jzj@localhost jzjtest]$ java -jar  demo-0.0.1-SNAPSHOT.jar 

启动成功, 端口 8078

image.png

测试 TestController, 执行 curl 127.0.0.1:8078/temp/ping, 返回pong

[jzj@localhost jzjtest]$ curl 127.0.0.1:8078/ping
pong
[jzj@localhost jzjtest]$ 

查看 java进程 , demo 进程PID 是6380

[jzj@localhost jzjtest]$ ps -ef |grep java
jzj        6380   5714  2 01:14 pts/1    00:00:05 java -jar demo-0.0.1-SNAPSHOT.jar
jzj        6652   6407  0 01:18 pts/2    00:00:00 grep --color=auto java
[jzj@localhost jzjtest]$ 
[jzj@localhost jzjtest]$ jps
6380 jar
6671 Jps

2.启动模拟程序, 升高CPU

执行 curl 127.0.0.1:8078/cpu, 让线程开始计算,模拟CPU升高 [jzj@localhost jzjtest]curl127.0.0.1:8078/cpuok[jzj@localhostjzjtest] curl 127.0.0.1:8078/cpu ok[jzj@localhost jzjtest]

2.1 top 命令查看进程 最高消耗CPU的进程

此刻, 我们利用top命令来定位CPU 升高的问题 , 我们解析下 top 打印进程信息及cpu信息

  • CPU 的load average 负载时0.95 也就是 95%
  • 此刻 一共 1个任务在running, 210个线程 sleep
  • 进程最高的 进程PID时 7198
top - 01:40:27 up  3:48,  4 users,  load average: 1.50, 1.37, 1.05
Tasks: 210 total,   3 running, 207 sleeping,   0 stopped,   0 zombie
%Cpu(s): 99.7 us,  0.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1863004 total,   132724 free,  1073776 used,   656504 buff/cache
KiB Swap:  2097148 total,  2096884 free,      264 used.   574696 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                   
  7198 jzj       20   0 2502788 119248  13556 S 98.7  6.4   3:16.74 java                                      
  1516 root      20   0  415924 137764  60148 S  0.7  7.4   0:36.55 X                                         
  2195 jzj       20   0 3130516 300892  93748 S  0.3 16.2   1:41.85 gnome-shell                               
     1 root      20   0  128600   7256   4220 S  0.0  0.4   0:02.03 systemd                                   
     2 root      20   0       0      0      0 S  0.0  0.0   0:00.00 kthreadd                                  
     4 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 kworker/0:0H              
2.2 top -Hp pid 查看进程中最高的线程

打印进程 7198 中最高的线程信息, 我们可以看到

  • 进程内,CPU的 98.7 消耗都是7154的 线程ID消耗的
[jzj@localhost jzjtest]$ top -Hp 7198

top - 01:40:38 up  3:48,  4 users,  load average: 1.43, 1.35, 1.05
Threads:  19 total,   1 running,  18 sleeping,   0 stopped,   0 zombie
%Cpu(s):100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1863004 total,   132972 free,  1073528 used,   656504 buff/cache
KiB Swap:  2097148 total,  2096884 free,      264 used.   574944 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND                                   
  7574 jzj       20   0 2502788 119248  13556 R 93.3  6.4   3:21.92 pool-1-thread-1                           
  7198 jzj       20   0 2502788 119248  13556 S  0.0  6.4   0:00.00 java                                      
  7199 jzj       20   0 2502788 119248  13556 S  0.0  6.4   0:01.94 java                                      
  7200 jzj       20   0 2502788 119248  13556 S  0.0  6.4   0:00.11 VM Thread     
2.3 printf %x 线程ID 把它转换为16进制

我们看到 7574 的线程,消耗最高, 但是7574在 jstack日志中是以 16进制形式展示的,所以我们要做下转换

[jzj@localhost jzjtest]printf1d96[jzj@localhostjzjtest] printf %x 7574 1d96 [jzj@localhost jzjtest]

2.4 jstack -F -l pid > a.txt 打印堆栈日志

我们使用命令 jstack -F -l 7574 > a.txt 打印堆栈日志

[jzj@localhost jzjtest]$ jstack -F -l 7574 >a.txt
sun.jvm.hotspot.debugger.DebuggerException: sun.jvm.hotspot.debugger.DebuggerException: get_thread_regs failed for a lwp
	at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal$LinuxDebuggerLocalWorkerThread.execute(LinuxDebuggerLocal.java:163)
	at sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal.getThreadIntegerRegisterSet(LinuxDebuggerLocal.java:482)
	at sun.jvm.hotspot.runtime.JavaThread.getCurrentFrameGuess(JavaThread.java:256)
	at sun.jvm.hotspot.runtime.JavaThread.getLastJavaVFrameDbg(JavaThread.java:218)
	at sun.jvm.hotspot.tools.StackTrace.run(StackTrace.java:86)
	at sun.jvm.hotspot.tools.StackTrace.run(StackTrace.java:45)

3.分析jstack日志

打开刚才 另存为的a.txt日志

查看分析日志, 我们重点关注 7574 这个线程的

ttaching to process ID 7574, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.362-b08
Deadlock Detection:

No deadlocks found.

Thread 7576: (state = BLOCKED)
Error occurred during stack walking:

Locked ownable synchronizers:
    - None

Thread 7575: (state = BLOCKED)
Error occurred during stack walking:

Locked ownable synchronizers:
    - <0x00000000e4036668>, (a java/util/concurrent/ThreadPoolExecutor$Worker)

Thread 7574: (state = IN_JAVA)
 - com.jzj.jvmtest.mythread.MyTask.run() @bci=12, line=12 (Compiled frame; information may be imprecise)
 - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=95, line=1149 (Interpreted frame)
 - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=624 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=750 (Interpreted frame)

Locked ownable synchronizers:
    - <0x00000000e401d538>, (a java/util/concurrent/ThreadPoolExecutor$Worker)

Thread 7573: (state = BLOCKED)
Error occurred during stack walking:

Locked ownable synchronizers:
    - None

Thread 7199: (state = BLOCKED)
Error occurred during stack walking:

我们找到 7574线程 ,分析如下

Thread 7574: (state = IN_JAVA)
 - com.jzj.jvmtest.mythread.MyTask.run() @bci=12, line=12 (Compiled frame; information may be imprecise)
 - java.util.concurrent.ThreadPoolExecutor.runWorker(java.util.concurrent.ThreadPoolExecutor$Worker) @bci=95, line=1149 (Interpreted frame)
 - java.util.concurrent.ThreadPoolExecutor$Worker.run() @bci=5, line=624 (Interpreted frame)
 - java.lang.Thread.run() @bci=11, line=750 (Interpreted frame)
  • 7574 线程
  • com.jzj.jvmtest.mythread.MyTask.run() 出问题的地方在这里
  • 定位问题 线程出现了问题,消耗CPU最高
  • 如果没有显示 7574线程,我们可以用内存地址
  • 比如 之前的 printf %x 7574 = 1d96
  • 用 1d96 在 jstack日志中 查看线程相关信息

本文我们 在Linux环境下,模拟了程序,进行了CPU过高的时候,如何通过top命令和jstack命令定位问题,并且进行了实战,帮助大家快速的解决CPU过高的问题,但是jstack命令也不是无敌的,有时候单纯的靠jstack,在实际项目中是无法定位问题的