1. 概述
本文将深入探讨Java程序中高CPU使用率问题。我们将分析潜在的根本原因,并提供相应的排查方法,帮助开发者快速定位和解决这类性能问题。
2. 什么才算高CPU使用率
在深入分析前,我们需要明确高CPU使用率的定义。毕竟,这个指标高度依赖程序的具体任务场景,正常情况下CPU使用率可能在0%-100%间大幅波动。
本文重点关注以下场景:
- Windows任务管理器或Unix/Linux的
top
命令显示CPU使用率持续达到90%-100% - 高负载持续时间从几分钟到几小时不等
- 这种高负载并非预期内的密集计算导致
⚠️ 特别注意:程序在执行科学计算或大数据处理时短暂100%CPU使用率是正常的,我们讨论的是非预期的、持续的高负载。
3. 可能的根本原因
导致CPU高负载的潜在原因多种多样,有些源于我们的代码实现,有些则由系统状态或资源利用异常引发。
3.1. 实现错误
首要排查点是代码中的无限循环。由于多线程机制,即使存在无限循环,程序仍可能保持响应状态。
一个典型的踩坑场景:运行在应用服务器(如Tomcat)的Web应用。虽然我们可能没有显式创建新线程,但应用服务器会为每个请求分配独立线程。这导致:
- 即使部分请求陷入循环,服务器仍能正常处理新请求
- 给人造成"系统运行正常"的假象
- 实际上应用性能已严重下降,当阻塞线程足够多时可能导致系统崩溃
3.2. 糟糕的算法或数据结构
另一个常见问题是使用了性能不佳或不匹配当前场景的算法/数据结构。看个简单粗暴的例子:
List<Integer> generateList() {
return IntStream.range(0, 10000000).parallel().map(IntUnaryOperator.identity()).collect(ArrayList::new, List::add, List::addAll);
}
这段代码使用ArrayList
生成了包含1000万个数字的列表。现在我们访问接近末尾的元素:
List<Integer> list = generateList();
long start = System.nanoTime();
int value = list.get(9500000);
System.out.printf("Found value %d in %d nanos\n", value, (System.nanoTime() - start));
由于ArrayList
基于数组实现,索引访问极快,输出类似:
Found value 9500000 in 49100 nanos
但如果将实现改为LinkedList
:
List<Integer> generateList() {
return IntStream.range(0, 10000000).parallel().map(IntUnaryOperator.identity()).collect(LinkedList::new, List::add, List::addAll);
}
同样的访问操作会变得极其缓慢:
Found value 9500000 in 4825900 nanos
仅仅一个数据结构的改动,程序性能就下降了100倍!
虽然我们不会主动做这种改动,但:
- 不了解
generateList
使用场景的其他开发者可能引入此变更 - 我们可能甚至不拥有该API的实现权,无法控制其内部实现
3.3. 频繁的大规模GC周期
有些原因与代码实现无关,甚至超出我们的控制范围,典型代表就是频繁的大规模垃圾回收。
以聊天室应用为例:用户每收到一条消息就会生成一个通知对象。在用户量少时,简单实现能正常工作。但当用户量增长到数百万级别,且每个用户加入多个聊天室时:
- 通知对象的数量和生成速率将急剧增加
- 堆内存迅速饱和,触发stop-the-world垃圾回收
- JVM清理堆期间系统完全失去响应,严重影响用户体验
4. CPU问题排查方法
从上述案例可见,仅靠代码审查或调试往往无法解决这类问题。我们需要借助专业工具来定位问题根源。
4.1. 使用性能分析器
性能分析器(Profiler)始终是可靠的选择。无论是GC问题还是无限循环,分析器都能快速定位热点代码路径。
市场上有多种性能分析工具,包括商业和开源方案。推荐组合:
- Java Flight Recorder(JFR)
- Java Mission Control(JMC)
- Diagnostic Command Tool
这套工具能帮助我们可视化排查性能问题。
4.2. 线程分析
当无法使用分析器时,可以通过线程分析定位问题。具体步骤取决于操作系统和环境,但通常包含两步:
- 使用工具显示所有运行线程的PID和CPU占用率,识别问题线程
- 使用JVM工具显示所有线程的当前堆栈信息,匹配问题PID
以Linux的top
命令为例:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3296 User 20 0 6162828 1.9g 25668 S 806.3 25.6 0:30.88 java
记录Java进程的PID(3296)。这确认了高CPU占用,但需要进一步定位具体线程。执行top -H
查看所有线程:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3335 User 20 0 6162828 2.0g 26484 R 65.3 26.8 0:02.77 Thread-1
3298 User 20 0 6162828 2.0g 26484 R 64.7 26.8 0:02.94 GC Thread#0
3334 User 20 0 6162828 2.0g 26484 R 64.3 26.8 0:02.74 GC Thread#8
3327 User 20 0 6162828 2.0g 26484 R 64.0 26.8 0:02.93 GC Thread#3
发现多个GC线程和我们的业务线程Thread-1
(PID 3335)在消耗CPU。使用jstack获取线程转储:
jstack -e 3296
在输出中通过线程名或十六进制PID查找Thread-1
:
"Thread-1" #13 prio=5 os_prio=0 cpu=9430.54ms elapsed=171.26s allocated=19256B defined_classes=0 tid=0x00007f673c188000 nid=0xd07 runnable [0x00007f671c25c000]
java.lang.Thread.State: RUNNABLE
at com.baeldung.highcpu.Application.highCPUMethod(Application.java:40)
at com.baeldung.highcpu.Application.lambda$main$1(Application.java:61)
at com.baeldung.highcpu.Application$$Lambda$2/0x0000000840061040.run(Unknown Source)
at java.lang.Thread.run([email protected]/Thread.java:829)
关键点:PID 3335的十六进制是0xd07,对应线程的nid值。根据堆栈信息,我们就能精确定位问题代码并着手修复。
5. 总结
本文分析了Java程序高CPU使用率的常见根因,通过实际案例展示了问题场景,并提供了两种实用的排查方法:
- ✅ 使用性能分析器进行可视化分析
- ✅ 通过线程分析定位问题代码
掌握这些方法后,面对CPU高负载问题时就能快速定位根源,避免在无效的代码审查上浪费时间。
本文源码可在GitHub获取。