近期发现一个java进程的cpu占用接近100%。
开始简单地认为是由于给java分配的内存不足,从而导致频繁GC。
于是首先的处理方式就是直接给该java程序分配更多的内存,然而进程启动没几分钟,cpu占用再次接近100%,看来问题没这么简单。
一. 分析 java 占用 cpu 过高的原因
1. 通过top命令直接查到该java进程的进程ID,可以看到进程ID为26260。
-bash-4.2$ top
top - 10:59:04 up 40 days, 19:54, 2 users, load average: 6.61, 5.06, 5.27
Tasks: 92 total, 1 running, 91 sleeping, 0 stopped, 0 zombie
%Cpu(s): 99.8 us, 0.2 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8010372 total, 640004 free, 5507184 used, 1863184 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 2194908 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
26260 root 20 0 3631872 189640 16676 S 98.3 2.4 5:34.30 java
2. 再次通过top命令找到该进程中占用cpu较高的线程,该线程的线程ID为26275。
[root@aliyun dancen]# top -H -p 26260
top - 15:03:57 up 40 days, 23:59, 2 users, load average: 1.08, 1.07, 1.11
Threads: 27 total, 1 running, 26 sleeping, 0 stopped, 0 zombie
%Cpu(s): 53.3 us, 0.0 sy, 0.0 ni, 46.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8010372 total, 447420 free, 5668448 used, 1894504 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 2033320 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
26275 root 20 0 3678404 326680 17116 R 99.9 4.1 248:24.00 java
26260 root 20 0 3678404 326680 17116 S 0.0 4.1 0:00.00 java
26261 root 20 0 3678404 326680 17116 S 0.0 4.1 0:01.54 java
26262 root 20 0 3678404 326680 17116 S 0.0 4.1 0:00.06 java
3. 通过printf命令得出该线程ID的十六进制表示为66a3。
[root@aliyun dancen]# printf '%x\n' 26275
66a3
4. 使用jstack命令打印该java进程的线程堆栈信息,该命令需要root权限来执行。
[root@aliyun dancen]# jstack 26260 > jstack.log
5. 在jstack.log中找出本地ID,即nid为66a3的线程信息。
[root@aliyun dancen]# cat jstack.log | grep 66a3 -A 10
"pool-3-thread-1" #12 prio=5 os_prio=0 tid=0x00007f0654780000 nid=0x66a3 runnable [0x00007f063c722000]
java.lang.Thread.State: RUNNABLE
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.poll(ScheduledThreadPoolExecutor.java:809)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1066)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
"Log4j2-TF-5-Scheduled-1" #9 daemon prio=5 os_prio=0 tid=0x00007f06545de800 nid=0x66a2 waiting on condition [0x00007f06441ca000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
可以看出问题出在ScheduledThreadPoolExecutor这个类的内部类DelayedWorkQueue的poll方法。
二. ScheduledThreadPoolExecutor bug 介绍
从上面可以看到,ScheduledThreadPoolExecutor这个类的内部类DelayedWorkQueue的poll方法消耗了大量的cpu资源。
查看代码中用到ScheduledThreadPoolExecutor的地方:
int corePoolSize = 0;
ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(corePoolSize);
这里声明了一个corePoolSize为0的线程池,这是有点诡异的地方,但是是允许的。
然而,问题就出在这个诡异点,这里触发了java的一个bug:
JDK-8129861:
ScheduledThreadPoolExecutor with corePoolSize = 0 causes 100% load on one CPU core
该bug即使用corePoolSize为0的ScheduledThreadPoolExecutor,会导致cpu飙升,已在java 9修复。具体描述请参见:
https://bugs.openjdk.java.net/browse/JDK-8129861