Skip to content

经典线上排查实战(CPU飙高与内存泄漏) ​


❓ 面试官:如果线上系统突然变得非常卡,接口响应极慢,你作为后端主程该怎么排查处理?(线上 CPU 飙高排查实战) ​

频率:🔥🔥🔥🔥🔥

这是考核候选人是否有过真实生产环境维护经验的试金石问题。

💡 一句话总结(先抛结论): 遇到这种紧急情况,排查思路分为四步:看大盘(找异常节点) -> 找耗性能进程(PID) -> 找元凶线程(TID) -> jstack 抓取堆栈定位具体代码行。

📝 详细的四步排查大法(面试高分剧本):

第一步:找元凶进程(PID)

  • 登录出现告警的服务器,敲 top 命令,看系统资源大盘。
  • 如果 load average(系统负载)很高,并且列表里有一个 Java 进程(比如 Tomcat 或者是我们的 SpringBoot jar 包进程)的 CPU 占用率达到了 100% 甚至更多。
  • 记下它的进程 ID(假设 PID = 1001)。

第二步:找元凶线程(TID)

  • 既然是 Java 进程占用过高,那肯定是进程里面的某一个线程在作怪(比如死循环)。
  • 敲 top -Hp 1001。这会展示出进程 1001 下面的所有线程的资源占用排行。
  • 找到消耗 CPU 最高的那个线程,记下它的线程 ID(假设 TID = 1010)。

第三步:将 TID 转成十六进制

  • 因为 JVM 打印的堆栈信息里,线程 ID 是用十六进制表示的,我们需要转换一下。
  • 用命令 printf "%x\n" 1010 把十进制的 1010 转成十六进制,得到 3f2。

第四步:通过 jstack 定位死循环代码

  • 最关键的一步来了!敲 jstack 1001 | grep -A 20 '3f2'(把进程栈导出来,并在里面过滤寻找这个十六进制的线程 ID,显示前后 20 行代码调用信息)。
  • 结果: 这时候控制台上就会清晰地打印出这个疯狂占用 CPU 的线程正在执行哪个类的哪一行代码。

🔥 常见场景(追问加分项): "面试官,根据我的实战经验,定位到代码行后,通常是这两种情况导致 CPU 飙高:"

  1. 业务死循环或复杂计算:比如某一段 while(true) 的退出的条件没写对,导致死循环;或者是写了一个极其复杂的正则表达式去解析长文本,发生了“正则回溯”导致 CPU 打满。修复代码重启即可。
  2. 频繁 Full GC:如果定位到的线程是 GC Task Thread,那说明 CPU 飙高只是表象,真实情况是内存快撑爆了。JVM 在疯狂地执行垃圾回收企图清理内存,但每次只能回收一点点,导致陷入 GC 停顿 -> 应用变卡 -> 继续 GC 的死亡循环。这时候的排查方向就要从 CPU 转向排查 OOM(内存溢出)了。

❓ 面试官:线上系统报错 java.lang.OutOfMemoryError(OOM),服务直接挂了,你是怎么排查的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): OOM 发生时绝对不能靠猜。核心手段是获取系统崩溃瞬间的 Heap Dump(堆内存快照)文件,然后利用可视化分析工具(如 Eclipse MAT)顺藤摸瓜,找出是哪个大对象把内存撑爆了。

📝 详细排查实战流程:

  1. 事前防御:开启自动 Dump(必须要有)

    • "在我的项目中,Java 启动脚本里一定会加上这几个参数: -XX:+HeapDumpOnOutOfMemoryError(发生 OOM 时自动生成 dump 文件) -XX:HeapDumpPath=/data/log/heapdump.hprof(指定快照生成的路径)。"
    • 如果没有配置这些,进程 OOM 挂掉后现场就完全丢失了,排查难度极大。
  2. 事发取证

    • 发生 OOM 报警后,去服务器指定的目录下,把那个几百兆甚至几 GB 的 .hprof 快照文件下载到本地电脑进行分析。
  3. 事后分析(使用 MAT)

    • 打开 Eclipse MAT(Memory Analyzer Tool)加载快照文件。
    • 第一眼看它自动生成的 "Leak Suspects"(泄漏嫌疑人报告)。它会用一个很直观的大饼图告诉你问题出在哪。比如:"有一个 ArrayList 占据了系统 85% 的内存,里面装了 200 万个 UserEntity 对象"。
    • 然后点击 "Path to GC Roots"(查看到达 GC Root 的强引用链),就能看到是谁在持有这个 List 并且不释放。
  4. 定位与总结(常见 OOM 原因剖析)

    • 通过引用链,最后能精确定位到是哪一个 Controller 或 Service 中的代码。
    • 典型的故障场景分享:
      • 场景 A(大对象/无分页):同事写的一段查询代码 select * from user 忘了加 limit 分页条件。恰好表里有几百万条数据,一次性全部加载到内存里转换成 List,瞬间就把堆内存撑爆了。解决:强制要求所有批量查询必须分页。
      • 场景 B(内存泄漏/ThreadLocal):用了一个全局的静态 Map 或者 ThreadLocal 缓存数据,但是每次用完忘记执行 remove() 清理。对象用完了却一直被强引用着无法回收,时间一长,内存就渐渐满了直到 OOM。解决:规范代码,确保 try-finally 块里执行清理逻辑。
      • 场景 C(流未关闭):频繁进行 Excel 导出或者大文件上传下载,没处理好流的关闭。