Java高级后端 · 全套面试通关手册(线上故障排查)

1. CPU飙高排查【Java 后端线上故障排查】

面试核心:一套完整排查链路,从服务器定位 → 进程 → 线程 → 代码,生产实战流程。

一、整体排查步骤(背诵主线流程)

  1. 先用 top 命令查看整机资源,定位是哪个进程 CPU 高

  2. 找到高 CPU 进程 PID,查看该进程内部线程占用

  3. 导出线程栈,定位占用 CPU 的线程

  4. 翻译成可读线程 ID,查看栈信息,定位代码

  5. 结合业务,判断代码问题,修复

二、分步命令

(1)top

查看服务器 CPU、内存。按P按 CPU 排序,找到 CPU 高的 PID。

(2)top -Hp PID

查看该进程下所有线程,按 P 排序,找到高 CPU 线程 TID。

拿到 TID 后,转 16 进制:printf "%x\n" TID

(3)jstack PID

打印 Java 进程线程堆栈,找到 16 进制线程号,定位代码。

注意:jstack 要和进程 JDK 版本保持一致;高负载时 jstack 可能卡顿。

备选:jstat -gc PID 1000 观察 GC 情况,区分是代码业务逻辑 CPU 还是GC 导致 CPU 飙升。

三、两大类根因:业务代码 / GC 问题

类型 1:业务代码导致 CPU 高(线程持续跑)

典型场景:

  1. 死循环:while (true) 无退出条件;循环内没有 sleep,疯狂占用 CPU。

  2. 复杂计算、大量字符串拼接、正则匹配、大集合遍历。

  3. 频繁序列化 / 反序列化,大数据对象处理。

  4. 频繁创建大量临时对象,或者大量复杂排序。

栈特征:线程 RUNNABLE 状态,卡在业务方法。

类型 2:GC 频繁导致 CPU 飙高(非常高频考点)

现象:jstat 看到 YGC 频繁,FullGC 频繁。 原因:

  1. 内存分配不合理,堆太小;

  2. 代码大量创建短生命周期对象,快速填满新生代;

  3. 内存泄漏:集合长期持有对象无法释放;

  4. 大对象直接进入老年代,频繁触发 Full GC。

栈特征:多个 GC 工作线程占用大量 CPU。

区分判断口诀:看 jstat。GC 曲线持续上涨,GC 次数暴涨 → GC 问题;

GC 正常,业务线程 RUNNABLE 占 CPU → 代码死循环 / 复杂计算。

四、补充工具

  • arthas(阿里诊断工具,生产最常用)
XML 复制代码
# 查看进程CPU
thread -n 5
# 查看GC状态
gc
# 查看方法耗时
trace 包名.方法名

arthas 更方便,不用手动转 16 进制线程 ID,线上优先用 Arthas。

五、排查后解决方案

  1. 死循环:修复循环退出条件,增加休眠。

  2. GC 频繁:优化代码减少临时对象;调整堆参数;排查内存泄漏。

  3. 复杂计算:异步处理、缓存结果、减少重复计算。

高频深挖面试追问

Q1:top 看到 CPU100%,怎么区分是业务代码还是 GC?

答:使用 jstat -gc 观察 GC 指标。如果 YGC 频繁、老年代持续上涨,GC 线程占用高,就是 GC 问题;GC 次数正常,业务应用线程 RUNNABLE 占用 CPU,则是业务代码死循环或者密集计算。

Q2:jstack 拿到线程栈,如果是 GC 线程在跑,下一步怎么做?

答:dump 堆jmap -dump:format=b,file=xxx.hprof PID,下载 hprof 文件,用 MAT 分析,查找内存泄漏、大对象。

Q3:jstack 有什么注意事项?

高负载下多次 jstack 对比,单次栈可能只是瞬时状态;不要频繁执行,会暂停应用;最好 2~3 次间隔采样,看持续热点线程。

Q4:Arthas 中 thread -n 5 含义?

打印当前 CPU 最高的 5 个线程,直接看到线程栈,省去手动转十六进制,线上首选。

Q5:CPU 高但是 GC 正常,可能是什么原因?

死循环、大量循环遍历、复杂正则、大量编解码、密集计算。

一句话背诵总结

CPU 飙高排查流程:top 找进程 → top -Hp 找高 CPU 线程 → TID 转 16 进制,jstack 打印线程栈定位代码;

区分是业务死循环密集计算,还是频繁 GC;

GC 问题 dump 堆用 MAT 分析内存泄漏;生产推荐 Arthas 快速定位热点线程和方法。

2. OOM 内存溢出

OOM:OutOfMemoryError,JVM 没有足够内存分配对象,抛出该错误;

OOM ≠ 内存泄漏,内存泄漏是导致 OOM 最常见原因。

一、常见 OOM 类型

  1. Java heap space(堆 OOM,最常见) 堆内存不足,无法创建新对象。大多是内存泄漏,或是一次性分配超大对象。

  2. Metaspace(元空间 OOM,JDK8) 元空间存放类信息、常量、方法。原因:动态生成大量 Class(ASM/Cglib 反射、动态代理、热部署),类加载不回收。

  3. Unable to create new native thread(无法创建新线程) 创建线程过多。每个线程占用栈内存,线程数量耗尽系统资源。

  4. Direct buffer memory(直接内存 OOM) NIO 使用堆外内存,手动分配却忘记释放;堆外内存不受堆大小限制,但受系统总内存限制。

二、完整排查步骤(背诵主线)

1.查看应用日志,确认 OOM 异常类型,区分是堆 / 元空间 / 直接内存 / 线程栈 OOM。

2.JVM 启动参数增加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/xxx,OOM 发生自动 dump 堆快照 hprof 文件。

3.拿到 hprof 文件,使用 MAT(Memory Analyzer Tool)分析。

4.MAT 重点看:

  • Histogram 直方图:查看对象实例数量、占用内存,找出异常大对象

  • Dominator Tree 支配树:找出占据大量内存的对象,看是谁持有引用

  • Leak Suspects:MAT 自动给出疑似内存泄漏报告

5.定位代码:查看对象引用链,判断对象为什么无法被 GC 回收。

6.复现或查看业务逻辑,修复代码。

三、常见原因

1. Java heap space 堆 OOM

  1. 内存泄漏:集合(List/Map)静态全局集合,不断 add 元素,没有清理,对象一直持有引用无法 GC。

  2. 一次性加载大量数据:数据库查询select *一次性查出几十万数据放入内存集合。

  3. 堆内存设置过小,业务流量上涨,正常对象就撑满堆。

2. Metaspace 元空间 OOM

  • 频繁动态代理、CGLIB、ASM 动态生成类

  • 热部署框架,重复加载 class,旧类没有卸载

参数:-XX:MaxMetaspaceSize 设置元空间上限

3. Direct buffer memory 直接内存 OOM

NIO、Netty 使用 ByteBuffer 分配堆外内存;没有调用 release 释放。 堆外内存 GC 回收滞后,容易 OOM。

4. Unable to create new native thread

创建大量线程,线程栈占用操作系统内存。线程数量受操作系统最大进程数限制。

四、排查命令 & 工具

1.jmap -dump:format=b,file=xxx.hprof PID 手动 dump 堆(尽量业务低峰执行,会 STW)

2.jstat -gc PID 1000 持续观察堆内存、GC 变化,看老年代持续上涨

3.Arthas:heapdump 直接 dump 堆快照;memory查看内存使用情况

4.MAT:分析 hprof,定位泄漏对象、引用链

五、解决方案

  1. 集合用完及时清理,避免静态集合长期持有对象。

  2. 数据库大数据查询分页、流式读取,不要一次性加载全量数据到内存。

  3. 合理设置 JVM 堆、元空间大小,不要过大 / 过小。

  4. NIO/Netty 堆外内存使用完主动释放。

  5. 控制线程数量,使用线程池,禁止手动 new Thread 无限制创建线程。

高频深挖面试追问

Q1:OOM 和内存泄漏的区别?

内存泄漏:对象不再使用,但 GC 无法回收,内存缓慢上涨,最终耗尽内存引发 OOM 。 OOM 是结果;内存泄漏是最常见原因;OOM 也可以是一次性申请超大对象,不存在泄漏。

Q2:发生 OOM 之后,进程会直接退出吗?

不一定。抛出 OOM 的线程终止;其他线程继续运行。但内存已经不足,后续随时再次 OOM。建议配置自动 dump 后重启。

Q3:为什么元空间会 OOM?

元空间存 Class 信息。动态代理、热部署会大量创建 class;类只有在类加载器回收时才卸载。大量动态类导致元空间持续增长。

Q4:堆 dump 什么时候采集?

必须在 OOM 发生那一刻 dump。在 OOM 之后很久再手动 dump,泄漏对象可能已经被回收,看不到现场。所以 JVM 参数配置 HeapDumpOnOutOfMemoryError,OOM 自动 dump。

Q5:MAT 支配树是干嘛的?

支配树,看对象如果被回收,能释放多少内存。快速定位占用内存最大的对象,追踪引用链找到泄漏代码。

一句话背诵总结

OOM 分堆、元空间、直接内存、线程创建失败四类;

配置 JVM 参数 OOM 自动 dump 堆快照;

用 MAT 分析 hprof,通过直方图、支配树、引用链定位;

堆 OOM 大多是静态集合内存泄漏或者一次性加载大量数据;

元空间 OOM 多是动态类 / 热部署;堆外内存记得主动释放,优先使用线程池控制线程数量。

3. FullGC频繁

前置回顾:Full GC 会回收整个堆(新生代 + 老年代),会STW(Stop The World),暂停所有业务线程;频繁 FullGC 会造成接口卡顿、超时,是线上重大问题。

一、触发 FullGC 的常见原因(背诵)

1.老年代空间不足

对象晋升到老年代,老年代剩余空间放不下,触发 FullGC。

  • 短生命周期对象过多,快速占满新生代,大量对象快速晋升到老年代

  • 内存泄漏:长生命周期集合持续持有对象,老年代持续涨,占满堆

2.大对象直接进入老年代

超过-XX:PretenureSizeThreshold阈值的对象,绕过新生代直接进老年代;大量大对象快速填满老年代。

3.动态年龄判断

新生代对象年龄达到阈值晋升到老年代;动态年龄规则:同年龄对象总大小超过 survivor 一半,>= 该年龄全部晋升到老年代。

4.元空间满(JDK8 Metaspace)

元空间占用到达 MaxMetaspaceSize,触发 FullGC,尝试卸载类。

5.System.gc()

代码手动调用System.gc(),

建议禁用 :-XX:+DisableExplicitGC。

6.CMS

特有:CMS 并发失败、浮动垃圾,触发 Serial Old 兜底 FullGC(G1/ZGC 很少)

二、排查步骤(主线流程,面试口述)

1.用jstat -gc PID 1000持续观察

GC 指标:

  • FGC 次数、FGCT 耗时;看老年代 O 不断上涨,每次 FGC 后下降很快又涨上来,大概率内存泄漏

  • YGC 频繁,每次 YGC 后大量对象晋升到老年代

2.查看 GC 日志,定位 FullGC

触发原因(GC 日志加参数-Xloggc:xxx.log)

3.如果怀疑内存泄漏:

dump 堆jmap -dump:format=b,file=xxx.hprof PID,MAT 分析,找长存活对象、引用链

4.观察大对象:看是否一次性加载大批量数据(查询全表放入 List)

5.检查代码:有没有手动 System.gc 调用

6.查看元空间占用,判断是否类加载过多

三、区分两种典型现象

场景 1:FullGC 后内存下降,很快再次涨满(内存泄漏)

特征:每次 FullGC 能释放一部分内存,但短时间老年代迅速被占满,FGC 持续触发。

根源:对象不再使用,但存在强引用无法回收。

解决:MAT 分析堆快照,找到泄漏对象,清理集合引用。

场景 2:FullGC 后内存大幅下降,平稳一段时间(非泄漏)

特征:业务正常流量产生大量对象,一次性大量对象晋升老年代。

根源:堆参数不合理、大对象、短生命周期对象太多。

解决:优化代码减少临时对象;调整堆、晋升阈值。

四、解决方案(背诵)

1.代码层面(优先)

  • 避免一次性查询大量数据,分页 / 流式读取,不要一次性放入内存集合

  • 集合使用完毕,及时清除引用,静态集合谨慎使用

  • 减少循环内创建大量临时对象

  • 移除代码里System.gc(),JVM 参数禁用显式 GC

2.JVM 参数调优

  • 合理设置新生代大小,不要新生代过小,对象快速晋升到老年代

  • 合理设置PretenureSizeThreshold,控制大对象晋升规则

  • 设置MaxMetaspaceSize防止元空间触发 FullGC

3.架构层面

  • 大对象做缓存 / 异步处理,不占用 JVM 堆内存

  • 内存泄漏无法快速修复,可增加监控告警,定时重启兜底

五、高频深挖面试追问

Q1:FullGC 为什么 STW 时间很长?

FullGC 扫描整个堆,堆越大,STW 时间越长。生产堆不要设置过大(比如几十 G 堆,一次 FGC 停顿极长)。

Q2:YGC 频繁会不会引发 FullGC?

会。YGC 频繁,survivor 放不下,大量对象晋升到老年代,老年代持续填满,触发 FullGC。

Q3:G1 还会有 FullGC 吗?

G1 目标减少 FullGC,但依然会触发。比如混合收集跟不上内存增长、分配大对象失败,G1 会退化为 Serial Old 的 FullGC,STW 非常久。ZGC 几乎不会 FullGC。

Q4:老年代每次 FullGC 之后内存只释放一点点,是什么问题?

典型内存泄漏。大量对象是强引用,GC 无法回收,只能释放少量弱引用对象。老年代持续上涨,FGC 越来越频繁。

Q5:如何区分是内存泄漏还是正常业务流量导致 FullGC?

看 GC 后老年代内存水位。FGC 之后内存明显回落,属于业务正常对象;FGC 之后内存下降很少,很快又涨满,就是内存泄漏,需要 dump 堆分析。

一句话背诵总结

FullGC 回收整个堆,产生长时间 STW;

常见诱因:老年代占满、内存泄漏、大对象、元空间满、手动 System.gc;

jstat 观察 GC 指标,GC 日志定位触发原因;

FGC 后内存回落少,大概率内存泄漏,dump 堆用 MAT 排查;

优先优化代码,谨慎调 JVM 参数;G1 依然会 FullGC,ZGC 大幅降低 FullGC 概率。

4.死锁排查

死锁定义:多个线程互相持有对方所需要的锁,并且都不释放,形成循环等待,所有线程永久阻塞,业务卡住,CPU 不一定高。死锁四大必要条件,全部满足才会发生死锁。

一、死锁四大必要条件(必背)

  1. 互斥条件:资源是独占锁,同一时刻只能一个线程持有(synchronized、ReentrantLock 排他锁)。

  2. 持有并等待:线程已经持有锁,在不释放已有锁的情况下,继续去申请其他锁。

  3. 不可剥夺:已经拿到的锁,不能被其他线程强行抢占,只能持有者主动释放。

  4. 循环等待:线程之间形成闭环,A 等 B 的锁,B 等 A 的锁。

破坏任意一条,就可以消除死锁。工程上最常用:统一锁获取顺序,破坏循环等待。

二、Java 死锁排查完整流程(背诵主线)

  1. 现象:接口请求卡住、超时,线程一直阻塞,CPU 不高。

  2. 命令 jps 找到 Java 进程 PID

  3. jstack PID 打印线程栈。jstack 会自动检测死锁 ,在输出末尾找到Found one Java-level deadlock。

  4. 查看输出:找到线程名称、锁信息、代码行号,看两个线程各自持有的锁、等待的锁。

  5. 定位业务代码,分析获取锁顺序问题。

  6. 修改代码,调整锁顺序;重启验证。

Arthas 工具:thread命令,会直接识别死锁线程,无需手动解析堆栈。

三、典型死锁代码场景

场景:线程 1 先拿锁 A,再申请锁 B;线程 2 先拿锁 B,再申请锁 A。

java 复制代码
//线程1
synchronized(A){
    synchronized(B){
    }
}
//线程2
synchronized(B){
    synchronized(A){
    }
}

互相持有对方需要的锁,循环等待,死锁。

四、死锁与活锁、线程饥饿区分(面试高频对比)

  1. 死锁:线程阻塞,不再运行,互相等锁,永久卡住。

  2. 活锁:线程没有阻塞,一直重试,但始终无法执行成功(比如自旋锁互相谦让),CPU 占用。

  3. 线程饥饿:线程一直拿不到资源,长期得不到执行;例如低优先级线程,高优先级持续抢占资源。

五、如何避免死锁(背诵)

  1. 统一锁获取顺序(最核心):所有线程获取锁都按固定顺序,先 A 后 B,杜绝循环等待。

  2. 控制锁粒度,缩小锁范围,减少持有锁的时间。不要在锁内部调用外部接口。

  3. 使用带超时的锁 tryLock(time,unit),获取锁超时主动放弃,释放已拿到锁,避免永久等待。

  4. 避免嵌套锁,尽量不写多层 synchronized 嵌套。

  5. 代码评审阶段检查嵌套锁逻辑。

高频深挖面试追问

Q1:jstack 检测到死锁,进程会自动退出吗?

不会。发生死锁的线程卡住,但进程不会 crash。业务请求阻塞超时,服务还在运行,需要人工重启修复代码。

Q2:synchronized 和 ReentrantLock 哪个更容易产生死锁?

二者都可能死锁。synchronized 获取锁不能设置超时;ReentrantLock 的 tryLock 可以加超时,能够主动规避死锁。

Q3:怎么区分死锁和普通锁等待?

普通锁等待:一个锁被占用,其他线程排队;锁持有者执行完会释放,等待线程后续能拿到锁。 死锁:多个线程循环等待,谁都无法释放锁,永远无法解除,jstack 会明确标记 Found deadlock。

Q4:tryLock 为什么可以防止死锁?

获取锁设置超时时间,如果指定时间拿不到锁,线程主动放弃,并且释放自己已经获取的锁,打破持有并等待条件。

Q5:数据库也会发生死锁,和 Java 锁死锁一样吗?

原理相似,都是循环等待。MySQL InnoDB 行锁死锁,数据库引擎会自动检测,选择代价小事务回滚;Java 线程死锁 JVM 不会自动处理,需要人工干预。

一句话背诵总结

死锁四大条件:互斥、持有并等待、不可剥夺、循环等待;用 jstack 检测,输出会直接标记死锁;典型场景嵌套锁获取顺序不一致;优先统一锁顺序避免死锁;tryLock 带超时可主动释放锁;死锁线程阻塞,进程不会自动退出;区分死锁、活锁、线程饥饿。

5. 接口偶发超时

重点:偶发最难排查,不是必现。大概率不是代码逻辑 bug,多是资源抢占、锁等待、GC、网络、中间件抖动等问题。

一、排查思路(背诵主线流程)

1.查看日志:超时接口的调用链路日志、异常堆栈,看卡在哪个环节(DB、Redis、MQ、外部 HTTP 调用)

2.监控大盘:看 CPU、内存、GC、线程数、连接池指标,看超时时刻是否伴随指标异常

3.链路追踪(SkyWalking/Pinpoint):查看 span 耗时,定位是哪一段慢

4.抓线程栈:超时瞬间 jstack/arthas thread,看业务线程阻塞在哪里

5.区分:是服务内部慢,还是下游依赖响应慢、网络抖动

二、常见根因分类

1.JVM 层面:GC 停顿(非常高频)

FullGC / YGC 长时间 STW,业务线程暂停,接口超时。 特征:超时时间点,监控看到 GC 耗时突增。 排查:jstat、GC 日志。

2.锁等待(synchronized / ReentrantLock)

部分线程拿到锁长时间不释放(锁内慢查询、调用外部接口),其他请求排队等待锁,偶发超时。 特征:线程栈大量 WAITING/BLOCKED。

3.数据库相关

  1. 慢查询:SQL 偶尔走全表扫描(缓存失效、统计信息变更,索引失效)

  2. 行锁等待:并发更新同一行,事务持有行锁,其他请求阻塞

  3. 连接池耗尽:数据库连接池 maxActive 打满,请求排队拿连接

  4. 主从延迟,读从库读到旧数据,或者从库压力突增

4.中间件抖动(Redis / RabbitMQ / Zookeeper)

  1. Redis:大 key、热 key、网络抖动、Redis 持久化 RDB/AOF 阻塞;客户端连接池耗尽

  2. MQ:消费堆积,消息处理变慢

  3. ZK:会话超时,重新发起连接

5.外部 HTTP 接口调用

调用第三方接口偶发响应慢,没设置超时、没有熔断,当前线程一直等待,拖垮接口。

6.连接池耗尽(数据库、httpclient、dubbo 连接池)

连接用完,新请求排队等待连接,等待超时。

坑:超时只设置业务接口超时,没有设置下游调用超时。

7.网络问题

TCP 丢包、DNS 解析慢、网卡队列满、跨机房网络抖动;

现象:随机超时,无固定规律。

8.线程池耗尽

自定义线程池队列满、线程全部被慢任务占住,新任务排队超时。

三、排查工具

  1. Arthas:thread查看线程阻塞;trace跟踪方法耗时;watch看入参出参

  2. SkyWalking/Pinpoint:分布式链路追踪,精准定位耗时节点

  3. jstack:抓线程栈,看 BLOCKED、WAITING 线程

  4. 监控:GC、连接池、DB 慢日志、Redis 监控

  5. tcpdump:网络问题抓包

四、解决方案

  1. JVM:优化减少 FullGC,设置合理堆大小,增加 GC 告警

  2. 锁:缩小锁范围,锁内部禁止调用外部接口;尽量不使用大锁

  3. DB:优化 SQL,避免行锁冲突;合理设置连接池大小,连接超时

  4. 远程调用:必须设置超时时间,引入熔断降级(Sentinel/Hystrix)

  5. 中间件:排查大 key、热 key;合理设置客户端连接池

  6. 线程池:合理配置核心线程、队列,监控线程池队列堆积告警

  7. 网络:跨机房调用评估延迟,DNS 缓存

高频深挖面试追问

Q1:接口偶尔超时,日志没有异常堆栈,优先查什么?

优先 GC 和分布式链路追踪。GC STW 会暂停业务线程,不会打印异常栈;其次看线程栈,确认是否锁等待、连接池排队。

Q2:如何区分是本服务 GC 超时,还是下游接口慢?

链路追踪看 span:如果本服务内部 span 耗时很长,GC / 锁问题;如果是调用下游的 span 耗时高,下游依赖慢。

Q3:调用第三方接口,没设置超时会怎么样?

线程会无限阻塞,线程池线程被耗尽,后续所有请求排队,接口批量偶发超时。所有远程调用必须设置超时。

Q4:数据库行锁导致偶发超时是什么现象?

并发更新同一行数据,事务执行时间长,其他事务等待行锁;没有并发时接口很快,高并发才偶发超时。

Q5:连接池满导致超时怎么快速定位?

监控连接池活跃连接数,看超时瞬间活跃连接达到 max;线程栈大量卡在获取连接的代码。

一句话背诵总结

接口偶发超时重点排查 GC 停顿、锁等待、数据库慢 SQL / 行锁、中间件抖动、远程调用无超时、连接池 / 线程池耗尽;

优先链路追踪定位耗时节点,超时瞬间抓取线程栈;

远程调用强制设置超时,引入熔断降级,增加各项指标监控告警。

相关推荐
IT_Octopus1 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
Wang's Blog1 小时前
Java 中间件之 RabbitMQ 快速入门: MQ 常见技术选型对比
java·中间件·java-rabbitmq
蜗牛互联网1 小时前
Java Agent 工具调用的 allowlist、参数校验与调用预算
java·开发语言·人工智能·后端·oracle
大文说跨境1 小时前
多账号环境隔离方案技术选型:指纹浏览器、VPS 与云手机的三种架构对比
java·开发语言·前端
骉马代驾1 小时前
代驾系统长连接实战(二):心跳、断线重连与消息补偿的完整实现
java
程序员Sunday1 小时前
Spring @Transactional 没回滚?按代理调用、异常和传播行为排查
java·后端·spring
java资料站1 小时前
案例:Spring Ai/Alibaba《模拟面试器》项目案例
人工智能·spring·面试
此时不提桶,更待何时2 小时前
06-16-B-Kafka面试与生产事故实战
分布式·面试·kafka
冰暮流星2 小时前
sql之is null 运算符
java·数据库·sql