写点什么

惊悚,单个 java 进程占用 700% 的 CPU

用户头像
万里无云
关注
发布于: 2021 年 01 月 22 日
惊悚,单个java进程占用700%的CPU

背景


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


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

一、采用 top 命令定位进程


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


top
复制代码


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


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


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

此处按照 cpu 排序:


top -Hp 23602
复制代码


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


三、使用 jstack 命令定位代码

1.线程号转换为 16 进制

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


printf "%x\n"  30309
复制代码


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


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

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


jstack -l 29706 > ./jstack_result.txt 
复制代码

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<byte[]> dataQueue = new LinkedBlockingQueue<byte[]>(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 发现:

//取出队列中的头部元素,如果队列为空则调用此方法的线程被阻塞等待,直到有元素能被取出,如果等待过程被中断则抛出 InterruptedException

E take() throws InterruptedException;

//取出队列中的头部元素,如果队列为空返回 null

E 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 等。


总结

<font color=#999AAA >CPU 飙升问题定位的一般步骤是:

1.首先通过 top 指令查看当前占用 CPU 较高的进程 PID;

2.查看当前进程消耗资源的线程 PID: top -Hp PID

3.通过 print 命令将线程 PID 转为 16 进制,根据该 16 进制值去打印的堆栈日志内查询,查看该线程所驻留的方法位置。

4.通过 jstack 命令,查看栈信息,定位到线程对应的具体代码。

5.分析代码解决问题。


参考:

https://blog.csdn.net/qq_21127151/article/details/105554734

https://www.cnblogs.com/fengweiweicoder/p/10992043.html


更多精彩,关注我吧。


发布于: 2021 年 01 月 22 日阅读数: 20
用户头像

万里无云

关注

微服务,分布式,中间件 2018.08.14 加入

热衷技术,乐于分享,欢迎关注,一起前行。

评论

发布
暂无评论
惊悚,单个java进程占用700%的CPU