单个java进程占用700%的CPU???
发布日期:2021-05-06 15:16:23 浏览次数:32 分类:精选文章

本文共 2957 字,大约阅读时间需要 9 分钟。

背景

最近负责的一个项目上线,运行一段时间后发现对应的进程竟然占用了700%的CPU,导致公司的物理服务器都不堪重负,频繁宕机。

那么,针对这类java进程CPU飙升的问题,我们一般要怎么去定位解决呢?

单个java进程占用700%的CPU

达升笑2021-01-25 09:42:25

背景

最近负责的一个项目上线,运行一段时间后发现对应的进程竟然占用了700%的CPU,导致公司的物理服务器都不堪重负,频繁宕机。

那么,针对这类java进程CPU飙升的问题,我们一般要怎么去定位解决呢?

一、采用top命令定位进程

登录服务器,执行top命令,查看CPU占用情况,找到进程的pid

top1

很容易发现,PID为29706的java进程的CPU飙升到700%多,且一直降不下来,很显然出现了问题。

二、使用top -Hp命令定位线程

使用 top -Hp <pid> 命令(为Java进程的id号)查看该Java进程内所有线程的资源占用情况(按shft+p按照cpu占用进行排序,按shift+m按照内存占用进行排序)

此处按照cpu排序:

top -Hp 23602

很容易发现,多个线程的CPU占用达到了90%多。我们挑选线程号为30309的线程继续分析。

三、使用jstack命令定位代码

1.线程号转换为16进制

printf “%x\n” 命令(tid指线程的id号)将以上10进制的线程号转换为16进制:

printf "%x\n"  30309

转换后的结果分别为7665,由于导出的线程快照中线程的nid是16进制的,而16进制以0x开头,所以对应的16进制的线程号nid为0x7665

2.采用jstack命令导出线程快照

通过使用dk自带命令jstack获取该java进程的线程快照并输入到文件中: jstack -l > ./jstack_result.txt 命令(为Java进程的id号)来获取线程快照结果并输入到指定文件。

3.根据线程号定位具体代码

在jstack_result.txt 文件中根据线程好nid搜索对应的线程描述

cat jstack_result.txt |grep -A 100  7665

å¨è¿éæå¥å¾çæè¿°

根据搜索结果,判断应该是ImageConverter.run()方法中的代码出现问题

PS

这里也可以直接采用jstack <pid> |grep -A 200 <nid>来定位具体代码

$jstack 44529 |grep -A 200 ae24"System Clock" #28 daemon prio=5 os_prio=0 tid=0x00007efc19e8e800 nid=0xae24 waiting on condition [0x00007efbe0d91000]   java.lang.Thread.State: TIMED_WAITING (sleeping)    at java.lang.Thread.sleep(Native Method)    at java.lang.Thread.sleep(Thread.java:340)    at java.util.concurrentC.TimeUnit.sleep(TimeUnit.java:386)    at com.*.order.Controller.OrderController.detail(OrderController.java:37)  //业务代码阻塞点

四、分析代码解决问题

下面是ImageConverter.run()方法中的部分核心代码。

逻辑说明:
在while循环中,不断读取堵塞队列dataQueue中的数据,如果数据为空,则执行continue进行下一次循环。如果不为空,则通过poll()方法读取数据,做相关逻辑处理。

//存储minicap的socket连接返回的数据   (改用消息队列存储读到的流数据) ,设置阻塞队列长度,防止出现内存溢出//全局变量private BlockingQueue
dataQueue = new LinkedBlockingQueue
(100000);//消费线程@Overridepublic void run() { //long start = System.currentTimeMillis(); while (isRunning) { //分析这里从LinkedBlockingQueue if (dataQueue.isEmpty()) { continue; } byte[] buffer = device.getMinicap().dataQueue.poll(); int len = buffer.length;}

初看这段代码好像没什么问题,但是如果dataQueue对象长期为空的话,这里就会一直空循环,导致CPU飙升。

那么如果解决呢?
分析LinkedBlockingQueue阻塞队列的API发现:

//取出队列中的头部元素,如果队列为空则调用此方法的线程被阻塞等待,直到有元素能被取出,如果等待过程被中断则抛出InterruptedExceptionE take() throws InterruptedException;//取出队列中的头部元素,如果队列为空返回nullE poll();

这两种取值的API,显然take方法更时候这里的场景。

代码修改为:

while (isRunning) {   /* if (device.getMinicap().dataQueue.isEmpty()) {        continue;    }*/    byte[] buffer = new byte[0];    try {        buffer = device.getMinicap().dataQueue.take();    } catch (InterruptedException e) {        e.printStackTrace();    }……}

重启项目后,测试发现项目运行稳定,对应项目进程的CPU消耗占比不到10%。

CPU飙升的常见原因:

1.空循环,本文中的问题其实就这个原因导致的。

2.在循环的代码逻辑中,创建大量的新对象导致频繁GC
3.在循环的代码逻辑中进行大量无意义的计算。
简单来说,遇见CPU飙升的问题,就要仔细检查相关线程代码中的循环逻辑,比如for,while等。

总结

CPU飙升问题定位的一般步骤是:

1.首先通过top指令查看当前占用CPU较高的进程PID;
2.查看当前进程消耗资源的线程PID: top -Hp PID
3.通过print命令将线程PID转为16进制,根据该16进制值去打印的堆栈日志内查询,查看该线程所驻留的方法位置。
4.通过jstack命令,查看栈信息,定位到线程对应的具体代码。
5.分析代码解决问题。

上一篇:写代码有这16个好习惯,可以减少80%非业务的bug
下一篇:SpringBoot内置Tomcat浅析

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2025年04月18日 03时46分27秒