一、引言
我们在生产实践中或多或少遇见过CPU100%的问题,如果没有处理过这些问题,第一次遇见多少有点手忙脚乱不知道该怎么处理,而且这个问题也是面试过程中老生常谈的问题,掌握和解决这类问题对我们提升有很大的帮助,接下来我们就来聊一下有哪些问题会导致CPU100%,以及该如何处理解决这类问题。
二、常见业务场景
2.1 死循环导致 CPU 占用
死循环是最常见的导致 CPU 使用率 100% 的原因之一。程序没有正确终止循环或出现逻辑错误,导致程序在无限执行某些操作,消耗大量的 CPU 资源。
public class InfiniteLoopExample {
public static void main(String[] args) {
// 错误的循环条件,导致死循环
while (true) {
// 执行一些繁重的计算,消耗 CPU
int result = 0;
for (int i = 0; i < 1000000; i++) {
result += i;
}
}
}
}
优化建议:确保循环有合适的退出条件。如果需要某些长时间运行的任务,考虑将其分解为多个可管理的小任务,避免过度占用 CPU。
2.2 线程创建过多导致资源消耗
如果程序在高并发情况下没有有效的线程池管理,直接创建大量线程,每个线程都进行计算或 I/O 操作,会导致 CPU 资源被过度占用,最终可能引发 100% CPU 占用。
public class HighConcurrencyExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Thread(() -> {
while (true) {
// 执行高消耗的任务
Math.pow(Math.random(), Math.random());
}
}).start();
}
}
}
优化建议:使用线程池(如 ExecutorService
)管理线程。避免每次都创建新的线程,而是复用已存在的线程,合理控制并发数。
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
// 执行任务
});
}
2.3 频繁的垃圾回收 (GC) 导致 CPU 占用
如果应用程序在运行时频繁创建和销毁大量对象,JVM 的垃圾回收(GC)机制可能频繁触发。GC 过程会占用大量 CPU 资源,尤其是在大堆内存环境下。例如,大量临时对象的创建导致 JVM 不断触发 Full GC 或 Minor GC,导致 CPU 占用率飙升。
public class GCDemo {
public static void main(String[] args) {
for (int i = 0; i < 1000000; i++) {
// 创建大量临时对象,触发 GC
new String("Temporary Object");
}
}
}
优化建议:
- 减少短生命周期对象的创建,尽量重用对象,减少 GC 压力。
- 可以通过优化对象池、调整 JVM 的 GC 参数,合理管理内存使用,减少 GC 的频率。
- 监控 JVM 内存和 GC 行为,使用工具如 JVisualVM 进行性能分析。
2.4 高并发情况下的线程竞争(Lock)导致的 CPU 100%
在高并发环境下,如果多个线程频繁竞争同一资源,导致大量线程被阻塞并且无法释放 CPU,这样会导致 CPU 使用率居高不下,甚至达到 100%。例如,多个线程在同时访问同一个共享资源时,如果加锁不当(例如锁粒度过粗),可能导致多个线程无法并发执行,CPU 被锁操作占用。
public class ThreadLockExample {
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
synchronized (lock) {
// 执行一些计算,导致线程被阻塞
for (int j = 0; j < 1000000; j++) {
Math.pow(Math.random(), Math.random());
}
}
}).start();
}
}
}
优化建议:
- 尽量避免粗粒度锁,考虑使用更细粒度的锁(如读写锁
ReadWriteLock
)。 - 使用
ThreadPoolExecutor
等线程池进行任务调度,避免过多线程的创建。 - 使用无锁算法(如
CAS
)等优化竞争。
2.5 I/O 阻塞导致 CPU 占用
某些场景下,I/O 操作(如读取大文件、访问数据库等)可能会导致线程处于阻塞状态。如果应用使用了不当的 I/O 模型或阻塞 I/O 操作,会导致大量线程等待,从而加重 CPU 负担。如果应用程序需要频繁地进行 I/O 操作,但采用了阻塞式的 I/O 操作方式,可能会导致线程阻塞,从而影响 CPU 占用。
public class IOBlockingExample {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader("large_file.txt"));
String line;
while ((line = reader.readLine()) != null) {
// 模拟处理每一行数据,可能引发 I/O 阻塞
System.out.println(line);
}
reader.close();
}
}
优化建议:
- 使用 非阻塞 I/O(NIO)或 异步 I/O,避免 I/O 阻塞。
- 对于高并发 I/O 操作,考虑使用线程池或异步框架(如 Netty)处理请求。
三、使用jstack解决CPU100%
通过商量的了解我们都知道了CPU发生的场景都有那些,最常见的导致cou100%的就是死循环和死锁导致的,接下来我们就用一个死循环的场景来解决CPU100%,其他场景解决该问题的方式基本都是一样的。
@RequestMapping("loop")
public String loop() {
System.out.println("start");
while (true) {}
}
排查思路
(1)定位高负载的进程的PID:使用top命令或者top -c命令,查看当前 CPU消耗过高的进程PID。观察各个进程对资源的占有情况,我们可以直观的看到PID为1798的Java进程占用CPU最高,而且持续时间很长。
(2)根据 PID查出消耗 cpu最高的线程号: top -Hp 1798,按下P,进程按照 CPU使用率排序。找出最耗 CPU的线程,结果发现1798是就耗了99.9%。一般超过80%就是比较高的,80%左右是合理情况。这样我们就能得到CPU消耗比较高的线程id。
(3)将十进制的进程PID转为十六进制:通过上面的操作我们知道了哪个线程占用的资源的占用的最高,将其转换为十六进制。执行printf '%x\n' PID命令,将十进制的1832转换为十六制为0x728。
(4)根据线程号查出对应的 java线程:通过top -Hp命令我们可以知道了占用资源最高的线程PID,如果该线程的COMMAND显示不是java,则在使用jstack命令报错无法使用(因为jstack命令只针对java线程),此时我们找到执行top命令时查找到的占用资源最高的java进程号PID,执行jstack 进程号PID > threadDump.txt ,将指定 PID 的线程转储输出到 threadDump.txt
文件中。
jstack 1798 > threadDump.txt
(5)分析线程转储:打开 threadDump.txt
文件,全局查找第三步下的十六进制PID,如下图所示,我们可以清楚的看到导致CUP长时间100%的代码所在位置。
四、使用arthas解决CPU占用很高的问题
使用arthas解决CPU占用很高的问题,相比较jstack要简单快捷的很多。主要用到两个命令:
-
dashboard 命令查看TOP N线程
-
thread 命令查看堆栈信息
(1) 下载arthas-boot.jar
,然后用java -jar
的方式启动,此时它会列举出我们正在运行的java程序,输入出现问题的应用程序序号,我们这里只有一个输入1,回车。
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
(2)输入dashboard
命令可以看到是哪个线程占用cpu最高
(3)接下来输入thread -n 3
,表示最忙的前3个线程并打印信息。我们可以看到使用arthas和使用jstack得到的结果是一样的。
五、小结
本文我们主要例举了导致CPU占用很高的业务场景,同时给出了当我们线上CPU占用很高的时,解决问题的两种方式jstack和arthas,在这里我们还是推荐使用arthas去解决问题,更加的简单,简捷。