凌晨三点,城市在沉睡,而线上系统却突然 “炸锅”。监控大屏上,CPU 使用率的曲线如火箭般飙升,瞬间冲破 90% 红线,紧接着系统响应时间从原本的几十毫秒,一路攀升至数秒甚至十几秒。页面加载如同蜗牛爬行,用户的操作指令石沉大海,毫无回应。
电商平台的订单处理模块陷入僵局,新订单无法及时录入,支付流程也频频报错;物流系统的货物追踪信息停滞不前,司机和客户都在焦急等待最新动态;金融交易系统更是紧张,每一秒的卡顿都可能导致巨额资金的交易风险…… 业务量随着时间推移不断累积,系统却愈发不堪重负,报错信息像雪花般在后台疯狂滚动,整个线上业务濒临瘫痪。
这就是 CPU 飙高、系统反应慢带来的噩梦场景。相信不少 Java 开发者都经历过类似的 “惊魂时刻”,那种面对系统故障的紧张与焦虑,以及迫切想要快速解决问题的心情,至今仍历历在目。那么,当这一棘手问题出现时,我们该如何抽丝剥茧,找出背后的 “元凶”,让系统重归正常呢?别急,接下来就为大家详细拆解排查思路与实用方法 。
在深入排查之前,我们先来深入了解一下 CPU 飙高背后的底层原理,只有知其然且知其所以然,才能在排查时更加有的放矢。
在操作系统这个大舞台上,CPU 就像是一位忙碌的演员,同时要为多个进程或线程 “服务”。当它从一个任务切换到另一个任务时,就需要进行 CPU 上下文切换 。简单来说,上下文就是当前任务执行时 CPU 寄存器中的值、程序的状态以及堆栈中的内容。切换时,CPU 得先保存好当前任务的这些上下文信息,然后加载新任务的上下文,才能开始执行新任务。
在 Java 世界里,文件 IO、网络 IO 等操作都是 “上下文切换触发器”。以文件 IO 为例,当 Java 程序执行文件读取操作时,通常会经历一段阻塞期,线程只能眼巴巴地等待数据从磁盘读取到内存。此时,操作系统为了充分利用 CPU 资源,会将这个线程挂起,切换到其他可运行的线程上,这就导致了一次上下文切换。当数据读取完成后,操作系统又会将之前被挂起的线程重新调度到 CPU 上执行,又是一次上下文切换。同样,网络 IO 操作在等待网络响应时,也会触发类似的上下文切换流程。如果系统中存在大量频繁的文件 IO 或网络 IO 操作,上下文切换的次数就会急剧增加,CPU 忙着在各个任务间来回切换,真正用于执行有效业务逻辑的时间就被大大压缩,从而导致 CPU 使用率飙升,系统性能大幅下降。
如果说上下文切换过多是 CPU 的 “间接杀手”,那么 CPU 资源过度消耗就是 “直接凶器” 了。在 Java 程序中,创建大量线程和死循环是导致 CPU 资源被过度占用的两大 “罪魁祸首”。
想象一下,你创建了一个庞大的线程 “军团”,每个线程都需要占用一定的系统资源,如内存、CPU 时间片等。当线程数量超过系统的承载能力时,CPU 就会陷入疲于奔命的状态,不断地在线程之间进行调度,每个线程能分到的 CPU 时间变得少之又少,系统整体性能自然就会急剧下降。而且,过多的线程还可能引发激烈的资源竞争,进一步加剧 CPU 的负担。
死循环则是 CPU 的 “噩梦”。一旦线程陷入死循环,它就会像一个不知疲倦的 “永动机”,持续占用 CPU 资源,不给其他线程任何执行的机会。CPU 只能不停地执行死循环中的代码,无法去处理其他任务,最终导致 CPU 使用率直线上升,系统响应变得迟缓甚至完全失去响应。例如下面这段简单的 Java 代码:
public class DeadLoopExample {
public static void main(String[] args) {
while (true) {
// 这里没有任何退出条件,线程将一直执行这个空循环
}
}
}
当运行这段代码时,你会发现 CPU 使用率会迅速升高,因为这个线程一直在执行死循环,没有给其他线程和系统操作留下任何 CPU 时间 。
“工欲善其事,必先利其器” ,面对 CPU 飙高、系统反应慢的难题,掌握一些强大的排查工具是快速定位问题的关键。下面就为大家介绍几款在排查过程中常用的工具,它们各有所长,就像我们手中的 “瑞士军刀”,在不同场景下发挥着重要作用 。
top 命令堪称 Linux 系统下进程监控的 “元老级” 工具,它就像一位不知疲倦的 “系统管家”,实时展示系统中各个进程的资源占用情况,让我们对系统状态一目了然。
在排查 CPU 飙高问题时,top 命令的首要任务就是帮我们快速找出 CPU 占用率高的 Java 进程。打开终端,输入 “top” 命令并回车,瞬间,一个实时更新的进程信息界面就会出现在眼前。在这个界面中,众多进程信息按一定格式排列,其中 “% CPU” 列尤为关键,它清晰地显示了每个进程占用 CPU 的百分比。
默认情况下,top 命令会按照 CPU 使用率降序排列进程,所以那些 “CPU 大户” 进程会毫无保留地出现在列表顶部。我们只需在众多进程中,找到进程名称包含 “java” 的那一行,就能快速定位到可能存在问题的 Java 进程。比如,当我们看到某个 Java 进程的 “% CPU” 值高达 80% 甚至 90% 以上时,它就很可能是导致系统 CPU 飙高的 “罪魁祸首”,需要重点关注。
如果在 top 命令执行过程中,你想动态地对进程进行排序,以进一步确认问题,还可以通过一些交互命令来实现。按下 “P” 键,进程列表会立即按照 CPU 使用率重新排序,刚刚那个高 CPU 占用的 Java 进程会稳稳地占据榜首位置,让你看得更加清晰。
htop,作为 top 命令的 “豪华升级版”,在功能和用户体验上都有了大幅提升,就像是从 “普通轿车” 升级成了 “豪华跑车”,为我们的排查工作带来更多便利。
与 top 命令那略显 “朴素” 的界面相比,htop 的界面更加直观、丰富。它将系统信息划分成多个区域,每个区域都清晰展示了不同类型的关键信息。比如,在左上角区域,我们可以实时看到 CPU、物理内存和交换分区的使用情况,这些数据以直观的进度条形式呈现,让我们一眼就能把握系统资源的整体使用状态;右上角区域则展示了任务数量、平均负载和系统运行时间等信息,帮助我们从宏观角度了解系统的运行状况。
在进程展示区域,htop 更是下足了功夫。它不仅支持横向和纵向滚动浏览进程列表,让我们可以轻松查看所有进程的详细信息,还能完整显示进程的命令行,这对于区分同名进程或深入了解进程的启动参数非常有帮助。而且,htop 提供了强大的交互功能,通过各种快捷键,我们可以实现对进程的各种操作。按下 “F3” 键,就能快速搜索指定进程;按下 “F6” 键,便可以在多种排序方式之间自由切换,如按照 CPU 使用率、内存使用率、进程运行时间等进行排序,方便我们从不同角度分析进程状态 。
安装 htop 也非常简单,对于大多数 Linux 发行版,只需在终端中执行相应的包管理命令即可。在基于 Debian 或 Ubuntu 的系统上,运行 “sudo apt-get install htop”;在基于 Red Hat 或 CentOS 的系统上,运行 “sudo yum install htop”,就能轻松完成安装。安装完成后,在终端输入 “htop”,即可开启这个强大的进程监控之旅 。
ps 命令,全称为 “process status”,正如其名,它是一个专门用于报告当前进程状态的工具,就像一把 “万能钥匙”,可以帮我们打开了解进程详细信息的大门。
在排查 Java 进程 CPU 占用问题时,ps 命令能发挥重要作用。我们可以使用 “ps -ef | grep java” 命令,这条命令的含义是,首先通过 “ps -ef” 列出系统中所有进程的详细信息,然后通过管道符号 “|” 将这些信息传递给 “grep java”,“grep java” 会在这些信息中筛选出包含 “java” 关键词的进程,这样我们就能快速找到所有正在运行的 Java 进程,并获取它们的进程 ID(PID)、所属用户、启动时间等关键信息 。
如果我们想进一步查看某个 Java 进程的 CPU 占用情况以及其他状态信息,可以使用 “ps -mp PID -o THREAD,tid,time,% CPU” 命令,其中 “PID” 就是我们前面通过 “ps -ef | grep java” 获取到的 Java 进程 ID。这条命令会以线程为单位,展示该 Java 进程中各个线程的 CPU 占用情况、线程 ID(tid)、运行时间等信息,帮助我们深入分析进程内部的线程运行状态,找出可能导致 CPU 飙高的线程。
jstack 是 JDK 自带的一款命令行工具,堪称 Java 开发者深入线程世界的 “显微镜”,它可以打印指定 Java 进程的线程堆栈信息,让我们清晰看到每个线程在某个时刻的执行状态,从而快速定位线程相关的问题。
当系统出现 CPU 飙高时,有可能是线程陷入死循环、死锁或者长时间等待资源等原因导致的。这时,jstack 就派上用场了。首先,我们需要使用 jps 命令找到目标 Java 进程的 PID。jps 命令类似于 Linux 系统中的 ps 命令,专门用于列出当前运行的 Java 进程及其 PID。在终端输入 “jps”,就能看到所有正在运行的 Java 进程列表及对应的 PID。
找到 PID 后,执行 “jstack PID” 命令,这里的 “PID” 就是刚刚获取到的目标 Java 进程 ID。执行命令后,jstack 会输出该 Java 进程中所有线程的堆栈信息。在这些信息中,每个线程都有详细的描述,包括线程名称、线程 ID、线程状态以及调用栈信息。我们重点关注处于 “RUNNABLE” 状态且 CPU 占用率较高的线程,仔细查看它们的调用栈,看是否存在死循环代码。如果某个线程的调用栈中,某个方法被反复调用,且没有明显的退出条件,很可能就是这个方法中的代码陷入了死循环,导致 CPU 被持续占用 。
当怀疑系统存在死锁问题时,jstack 的输出也能提供关键线索。如果在输出信息中看到多个线程相互等待对方释放锁,形成了循环等待的局面,就可以确定发生了死锁,进而针对性地进行处理。
jstat,全称 “Java Virtual Machine statistics monitoring tool”,是 JDK 自带的一款轻量级工具,它就像一位 “GC 情况洞察者”,可以实时监控 Java 程序的垃圾回收(GC)情况,帮助我们判断 GC 是否正常,是否存在性能问题。
GC 在 Java 程序运行过程中起着至关重要的作用,它负责回收不再使用的内存空间,保证系统的内存使用效率。然而,如果 GC 出现问题,比如频繁进行 Full GC 或者 GC 耗时过长,就可能导致系统性能下降,甚至出现 CPU 飙高的情况。jstat 工具可以通过一系列参数,让我们深入了解 GC 的运行状态。
使用 “jstat -gcutil PID” 命令,其中 “PID” 为目标 Java 进程的 ID,该命令会输出 Java 堆内存中各个区域(Eden 区、Survivor 区、老年代等)的使用情况,以及 Young GC 和 Full GC 的次数、耗时等信息。通过分析这些数据,我们可以判断 GC 是否正常。如果发现 Young GC 的次数非常频繁,每次回收后 Eden 区和 Survivor 区的使用率仍然很高,可能意味着新生代内存分配不合理,对象创建和销毁过于频繁;如果 Full GC 的次数过多,且耗时较长,可能是老年代内存不足,或者存在内存泄漏问题,导致对象无法被及时回收 。
jmap,同样是 JDK 自带的强大工具,它就像是一位 “内存分析大师”,主要用于生成 Java 程序的内存快照,并分析堆内存的使用情况,帮助我们定位内存占用大的对象,找出可能导致内存泄漏的源头。
在排查 CPU 飙高问题时,内存问题也不容忽视。因为当系统内存不足时,可能会引发频繁的 GC,进而导致 CPU 使用率升高。jmap 工具可以通过 “jmap -dump:format=b,file=heapdump.hprof PID” 命令生成指定 Java 进程的内存快照,其中 “PID” 是目标 Java 进程的 ID,“heapdump.hprof” 是生成的内存快照文件名,可以根据实际需求进行修改。
生成内存快照后,我们可以使用专门的内存分析工具,如 Eclipse Memory Analyzer(MAT),来打开这个快照文件进行深入分析。MAT 工具能够以直观的图形界面展示堆内存中各个对象的占用情况,通过 “Dominator Tree” 视图,我们可以快速找到占用内存最多的对象,进一步查看这些对象的引用关系,判断是否存在对象无法被释放的情况,也就是内存泄漏。如果发现某个对象的实例数量异常多,且一直占用大量内存,就需要深入代码中查找原因,看是否存在对象创建后未被正确释放的问题 。
当线上系统遭遇 CPU 飙高、反应迟缓的紧急状况时,我们必须迅速行动,运用一系列有效的排查步骤,精准定位问题根源。下面,就让我们以一个实际案例为切入点,详细讲解实战排查的具体步骤 。
假设我们正在运维一个大型电商系统,突然接到监控告警,系统 CPU 使用率急剧上升。此时,我们首先登录到服务器,打开终端,输入 “top” 命令。瞬间,屏幕上出现了密密麻麻的进程信息,其中 “% CPU” 列的数据吸引了我们的目光,一个 PID 为 12345 的 Java 进程,其 “% CPU” 值竟然高达 90% 以上。
这个 Java 进程占用了大量的 CPU 资源,很可能就是导致系统 CPU 飙高的 “罪魁祸首”,我们先记下它的 PID,以便后续进一步分析。
如果你觉得 top 命令的界面不够直观,也可以使用 htop 命令。安装好 htop 后,在终端输入 “htop”,其界面会以更加友好的方式展示进程信息,我们同样可以轻松找到占用 CPU 高的 Java 进程,并且还能查看进程的更多详细信息,如命令行参数等,这对于我们判断进程的具体功能和运行情况非常有帮助 。
除了 top 和 htop,ps 命令也能帮助我们定位问题进程。执行 “ps -ef | grep java” 命令,系统会列出所有正在运行的 Java 进程及其相关信息,我们从中筛选出 CPU 占用率高的进程,确认其 PID,结果如下:
root 12345 1 90.0 20.7 1417908 208592 ? Sl 03:00 4:14.21 java -jar /opt/apps/ecommerce.jar
通过以上三种方式,我们都成功定位到了可能存在问题的 Java 进程,接下来就要深入进程内部,找出是哪些线程在 “捣乱” 。
确定了高 CPU 占用的 Java 进程后,下一步就是找出该进程中 CPU 消耗高的线程。我们使用 “top -Hp 12345” 命令(其中 12345 是前面获取到的 Java 进程 PID),这个命令会以线程为单位展示该进程中各个线程的 CPU 使用情况。按下 “P” 键,按照 CPU 使用率对线程进行排序,很快,我们就发现有一个线程的 CPU 占用率异常高,其 TID(线程 ID)为 23456。
除了 top -Hp 命令,我们还可以使用 “ps -mp 12345 -o THREAD,tid,time,% CPU” 命令来获取线程信息。该命令会输出线程的详细信息,包括线程 ID(tid)、运行时间和 CPU 占用率等,同样可以帮助我们找到 CPU 占用高的线程 。
在使用 jstack 命令分析线程堆栈信息时,需要将线程 ID 转换为十六进制格式。我们可以使用 “printf "% x\n" 23456” 命令进行转换,得到十六进制的线程 ID 为 5af8 。
找到了问题线程的十六进制 ID 后,接下来就使用 jstack 命令来分析线程堆栈,以定位到具体的问题代码行。执行 “jstack 12345 | grep 5af8 -A 30” 命令(其中 12345 是 Java 进程 PID,5af8 是十六进制的线程 ID,-A 30 表示显示匹配行及后面 30 行的内容),命令执行后,会输出一大段线程堆栈信息 。
假设我们得到的部分堆栈信息如下:
"http-nio-8080-exec-10" #23 prio=5 os_prio=0 tid=0x00007f2a3c00d800 nid=0x5af8 runnable [0x00007f2a2c96a000]
java.lang.Thread.State: RUNNABLE
at com.ecommerce.order.service.OrderServiceImpl.calculateOrderTotal(OrderServiceImpl.java:120)
at com.ecommerce.order.controller.OrderController.placeOrder(OrderController.java:80)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
...
从这段堆栈信息中,我们可以看到,这个线程当前正在执行 “com.ecommerce.order.service.OrderServiceImpl.calculateOrderTotal” 方法,具体代码位于 OrderServiceImpl.java 文件的 120 行。通过进一步查看这行代码及其相关逻辑,我们发现该方法中存在一个复杂的循环计算,并且没有进行有效的优化,这很可能就是导致 CPU 占用过高的原因 。
如果在堆栈信息中发现多个线程相互等待对方释放锁,形成了循环等待的局面,那就说明系统发生了死锁。例如
"Thread-1" #11 prio=5 os_prio=0 tid=0x00007f2a3c01e000 nid=0x6b78 waiting for monitor entry [0x00007f2a2d16d000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.ecommerce.product.service.ProductServiceImpl.updateProductStock(ProductServiceImpl.java:90)
- waiting to lock <0x000000076becdd10> (a com.ecommerce.product.dao.ProductDao)
at com.ecommerce.product.controller.ProductController.updateProduct(ProductController.java:60)
...
"Thread-2" #12 prio=5 os_prio=0 tid=0x00007f2a3c021000 nid=0x6b79 waiting for monitor entry [0x00007f2a2d26e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.ecommerce.product.dao.ProductDao.getProductById(ProductDao.java:40)
- waiting to lock <0x000000076becdd40> (a com.ecommerce.product.service.ProductServiceImpl)
at com.ecommerce.product.service.ProductServiceImpl.getProduct(ProductServiceImpl.java:50)
...
从上面的堆栈信息可以看出,Thread-1 正在等待获取 ProductDao 对象的锁,而 Thread-2 正在等待获取 ProductServiceImpl 对象的锁,并且它们相互持有对方需要的锁,形成了死锁,导致两个线程都无法继续执行,进而占用大量 CPU 资源 。
4.4 检查内存和 GC
在排查 CPU 飙高问题时,内存和 GC 情况也是不容忽视的重要因素。我们使用 “jstat -gc 12345” 命令(其中 12345 是 Java 进程 PID)来查看 GC 情况,命令执行后,会输出如下信息:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
1024.0 1024.0 0.0 1024.0 16384.0 15360.0 32768.0 20480.0 5632.0 5376.0 640.0 512.0 20 0.123 5 0.456 0.579
从这些数据中,我们可以看到 Young GC 的次数(YGC)为 20 次,Full GC 的次数(FGC)为 5 次,并且 Full GC 的耗时(FGCT)达到了 0.456 秒。如果发现 Full GC 的次数频繁且耗时较长,可能意味着老年代内存不足,存在内存泄漏问题,导致对象无法被及时回收,进而引发频繁的 Full GC,消耗大量 CPU 资源 。
为了进一步分析内存情况,我们使用 “jmap -dump:format=b,file=heapdump.hprof 12345” 命令生成内存快照,然后使用 Eclipse Memory Analyzer(MAT)工具打开这个快照文件。在 MAT 工具中,通过 “Dominator Tree” 视图,我们可以看到占用内存最多的对象。假设我们发现有一个名为 “com.ecommerce.user.UserService$UserCache” 的对象占用了大量内存,并且其实例数量异常多,这就很可能是内存泄漏的源头。进一步查看该对象的引用关系,发现存在一些无用的引用,导致这些对象无法被垃圾回收器回收,从而占用大量内存,引发内存相关的性能问题,间接导致 CPU 使用率升高 。
在 Java 系统运行过程中,业务线程引发的 CPU 飙高十分常见。例如,当业务代码中存在复杂度过高的算法,如在数据量庞大的集合中进行嵌套循环查找,每一次循环都会占用大量 CPU 资源,随着数据量增长,CPU 使用率会急剧上升 。还有一种典型场景是死循环,在多线程环境下,若某个线程因逻辑错误陷入死循环,它会持续占用 CPU 核心,导致其他线程无法获取足够资源,系统响应速度随之变慢。
针对这类问题,我们可以采用优化算法的方式来解决。比如将暴力查找算法替换为哈希查找、二分查找等高效算法;同时要养成良好的代码编写习惯,在循环中添加合理的终止条件判断,避免死循环的出现。在代码审查阶段,着重检查循环逻辑和复杂算法部分,提前发现潜在问题。
垃圾回收(GC)是 Java 自动内存管理的核心机制,但当 GC 过于频繁时,会导致 CPU 占用飙升。这可能是因为堆内存设置不合理,比如堆内存过小,对象创建速度过快,就会频繁触发 GC;或者代码中存在大量临时对象的创建,也会加重 GC 负担。
对此,我们可以通过调整 JVM 参数来优化。例如,使用-Xmx和-Xms参数合理设置堆内存的最大值和初始值,避免堆内存过小导致频繁 GC;还可以根据应用场景选择合适的 GC 收集器,比如吞吐量优先的Parallel GC,或者低延迟优先的CMS GC、G1 GC 。同时,优化代码逻辑,减少不必要的临时对象创建,合理管理对象生命周期,降低 GC 的频率和压力。
文件 IO、网络 IO 等操作是引发上下文切换过多的主要原因。在 Java 中,当线程进行文件读写或网络请求时,会从用户态切换到内核态,完成 IO 操作后再切换回来。如果系统中存在大量的 IO 密集型任务,频繁的上下文切换会消耗大量 CPU 资源。
优化 IO 操作是关键。可以采用异步 IO(如 Java NIO)替代传统的同步 IO,减少线程在 IO 操作上的阻塞时间;使用连接池管理数据库连接、网络连接,避免频繁创建和销毁连接带来的开销;对文件读写操作进行批量处理,减少 IO 操作次数。另外,合理控制线程池大小,避免创建过多线程导致上下文切换过于频繁,根据系统资源和任务类型,设置合适的线程数量。
回顾整个排查过程,当 Java 系统出现 CPU 飙高、反应慢的问题时,我们首先要借助top、htop等工具快速定位 CPU 占用高的进程,再通过jstack、jstat等命令深入分析线程堆栈和内存 GC 情况,从而找到问题根源并针对性解决。
在日常开发中,我们可以采取一系列预防措施。代码层面,编写高效算法、合理管理对象生命周期、优化 IO 操作;JVM 配置方面,根据业务场景和服务器资源合理设置堆内存大小、选择合适的 GC 收集器;系统架构层面,做好压力测试和性能调优,提前发现并解决潜在的性能瓶颈。只有这样,才能在面对 CPU 飙高问题时做到从容应对,保障 Java 系统稳定高效运行。如果你在实际排查中遇到其他特殊情况,欢迎在评论区分享,咱们一起探讨解决!