问题现象
某日收到告警,线上服务器CPU使用率持续飙升且居高不下,严重影响业务响应速度。于是立即登录服务器展开排查。
排查过程
1. 定位进程
首先使用 top 命令查看系统整体资源占用情况:
top
观察发现某个Java进程的CPU占用率异常高,记录下该进程的PID。
2. 确认程序来源
拿到PID后,通过以下命令确认该进程对应的具体程序:
ps -ef | grep <pid>
确认是Java应用程序出现了问题。
3. 定位问题线程
进一步查看该进程内部各线程的CPU占用情况:
top -H -p <pid>
找到CPU占用最高的线程,记录其线程ID(即 nid,十进制格式)。
4. 转储线程快照
使用 jstack 导出进程的所有线程栈信息,以便后续分析:
jstack <pid> > thread_dump.log
5. 换算线程ID格式
在 jstack 输出的线程快照中,线程ID使用的是十六进制格式,需要将之前记录的十进制 nid 转换为十六进制:
printf "%x\n" <nid>
6. 在快照中定位线程
用转换后的十六进制ID在 thread_dump.log 中查找对应线程。发现CPU占用高的是一组 GC 线程(Gang worker),共计8个左右,说明问题可能出在JVM堆内存上。
7. 查看GC实时状态
使用 jstat 实时监控JVM的GC情况:
jstat -gcutil <pid> 1000 10
将输出的GC日志喂给AI进行分析,结果发现:
- 老年代(Old Generation)使用率始终为100%
- 频繁触发Full GC,但内存始终无法回收
结合GC线程持续高占用的情况,初步推断存在 内存泄漏。
8. 导出堆快照
为深入分析内存占用情况,使用 jmap 导出当前堆内存快照:
jmap -dump:live,format=b,file=/root/heapdump.hprof <pid>
9. MAT分析堆快照
将 heapdump.hprof 文件下载到本地,使用 MAT(Memory Analyzer Tool) 打开进行内存分析。
通过MAT的 Leak Suspects 报告发现:大部分内存被某个数据库查询的结果集对象(ResultSet)占满。
10. 定位具体代码
在MAT中查看该对象的 Path to GC Root,追踪其引用链,发现该对象被一个名为 superior-platform-reboot-push-threat-pool1 的线程所持有。
回到之前的 thread_dump.log 线程快照,搜索该线程名,查看其当前栈信息。在栈帧中逐层查找,最终定位到项目自身的业务代码,发现是一个 Service 方法中的 MyBatis-Plus 的 Wrapper 对象没有设置任何查询条件。
问题原因
// 问题代码示例
LambdaQueryWrapper<Entity> wrapper = new LambdaQueryWrapper<>();
// 忘记添加 .eq(...) 等条件
List<Entity> list = mapper.selectList(wrapper); // 全表查询!
由于 Wrapper 对象缺少条件约束,selectList 执行了全表扫描,将整个表的数据一次性加载到内存中。数据量过大导致堆内存被撑爆,频繁触发 Full GC 且无法回收,最终引发CPU飙升。
解决方案
为 Wrapper 对象补充正确的查询条件:
LambdaQueryWrapper<Entity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Entity::getStatus, 1)
.ge(Entity::getCreateTime, startTime)
.le(Entity::getCreateTime, endTime);
List<Entity> list = mapper.selectList(wrapper);
总结与反思
- 编写查询代码时务必谨慎:尤其是使用 MyBatis-Plus 的
Wrapper或 JPA 的Example等工具,空条件可能导致全表查询。 - 大表查询必须加限制:对于数据量大的表,应配合分页(
Page)或限制返回条数。 - 监控告警要完善:CPU、GC频率、堆内存使用率等指标都应配置告警,尽早发现问题。
- 排查思路固化:进程 → 线程 → GC → 堆快照 → 引用链 → 代码定位,这条链路在类似问题中非常高效。
希望这次排查记录能对遇到类似问题的同学有所帮助。
评论
欢迎留下反馈,评论发布后会立即显示。