
生产问题排查CPU 飙高、内存泄漏、慢 SQL 三场景实战问题场景线上服务突然变慢CPU 飙到 90%日志刷屏但看不出根因——给你 15 分钟你会怎么排查读完收获① 掌握 CPU/内存/慢SQL 三类问题的标准排查路径 ② 获得可直接套用的 jstack/jmap/Arthas 分析模板 ③ 学会从现象→日志→代码→根因的逆向定位思维本文是《Java 后端核心知识图谱》系列第 1 篇共 172 篇。一、接口响应慢分层排查体系线上收到接口变慢的告警最大的忌讳是一上来就猜测原因。标准做法是分层排查逐层排除。第一层链路追踪定位通过 SkyWalking / Zipkin / Jaeger 查看调用链定位耗时最长的 Span。判断慢在哪个环节——应用代码、SQL 查询、Redis 操作、对外 HTTP 调用、MQ 发送。第二层应用层Arthas 三板斧dashboard# 查看 JVM 整体状态GC 频率、堆使用率、线程状态thread-n5# 定位 CPU 使用率最高的 5 个线程trace ClassName method# 追踪方法调用链输出每一步耗时watchClassName method{params, returnObj, throwExp}-x3# 观察入参/返回值monitor-c5ClassName method# 统计方法调用次数、成功/失败率、平均/最大耗时vmtool--actiongetInstances--classNamecom.xxx.Entity--limit10# 查看大对象数量trace是最常用的命令——它以树状结构输出方法调用链的每一步耗时一目了然地揭示哪个子调用耗时最长---[95.00%] DispatcherServlet.doDispatch() ---[92.00%] OrderController.createOrder() # 92% 时间在 Controller ---[88.00%] OrderService.createOrder() # 88% 在 Service ---[70.00%] deliveryService.query() # 70%瓶颈在此 ---[68.00%] jdbc.executeQuery() # SQL 耗时 68%由此可直接定位到 SQL 慢的问题。第三层数据库层常见慢原因未走索引EXPLAIN 显示typeALL全表扫描索引区分度差走了索引但rows扫描行数仍然很大回表过多需要通过覆盖索引消除回表锁等待通过SHOW ENGINE INNODB STATUS查看锁信息连接池耗尽通过 Druid / HikariCP 监控面板查看活跃连接数第四层缓存层检查 Redis 慢日志SLOWLOG GET 10排查大 key单个 Hash 包含数十万字段导致单次操作缓慢及热 key某个 key 的 QPS 极高导致单分片瓶颈问题。第五层外部依赖使用CompletableFuture将串行调用改为并行。配置超时与熔断Sentinel / Hystrix防止单个下游服务缓慢拖垮整个调用链路。二、工具速查表生产环境中机器负载高是常见问题掌握以下命令是高效排查的前提。JVM 层面场景工具关键命令CPU 飙高top jstack / Arthastop -H -p PID,thread -n 5内存泄漏jmap MAT / Arthasheapdump /tmp/dump.hprof, 分析 Dominator TreeGC 频繁jstat / GC 日志jstat -gcutil PID 1000,-Xlog:gc*死锁jstackjstack PID | grep BLOCKED -A 15类加载问题Arthasclassloader -t查看类加载器树方法耗时Arthas tracetrace com.xxx.Service method -n 5SQL 慢查Druid 监控 / SlowLogDruid 内置 SQL 监控面板Redis 慢Redis slowlogSLOWLOG GET 10OS 层面场景命令关注点机器整体负载top/htopload average1/5/15 分钟CPU 使用率us/sy/wa内存使用CPU 详情mpstat -P ALL 1各核心使用率分布判断是否为单核打满内存详情free -havailable vs freebuffer/cache 可回收磁盘 IOiostat -x 1%util 接近 100% 表示磁盘瓶颈await 为 IO 等待时间磁盘空间df -h各分区使用率日志爆满是常见非 OOM 的内存杀手网络连接netstat -antp | grep ESTABLISHED | wc -l连接数是否正常大量 TIME_WAIT 表示短连接过多端口监听ss -tlnp比 netstat 更快确认服务端口是否正常监听文件句柄lsof -p PID | wc -l句柄泄漏导致 “Too many open files”ulimit -n检查上限进程线程数cat /proc/PID/status | grep Threads线程数异常增长表示可能线程泄漏系统日志dmesg -T | tail、journalctl -u app-nameOOM Killer 日志、coredump、系统级错误排查经历的标准描述逻辑先用top获取整体状况——CPU 高内存高load average 异常若为 Java 进程则切入 Arthasdashboardthread -n 5若磁盘 IO 高则通过iostat确认——可能是大量日志写入或数据库 IO若为网络问题则通过netstat查看连接数——排查下游慢导致的连接堆积定位后JVM 层面使用 ArthasSQL 层面使用 EXPLAINOS 层面使用上述命令三、内存泄漏排查全流程线上内存持续增长最终导致 OOM应如何排查完整流程分为七步Step 1确认是泄漏还是正常增长使用jstat -gcutil PID 1000持续观察老年代使用率——若每次 Full GC 后老年代使用率持续上升如从 60% 到 70% 再到 80%则为泄漏若 GC 后能回落到正常水位仅为对象创建速度过快则属于正常情况需调优参数。Step 2在 OOM 发生前 dump 堆快照jmap-dump:formatb,file/tmp/heap.hprof PID堆内存较大时 jmap 可能触发 STW可加-F参数强制 dump。生产环境建议配置-XX:HeapDumpOnOutOfMemoryError实现自动 dump。Step 3使用 MAT 分析 Dominator Tree用 MAT 打开 hprof 文件点击 Dominator Tree支配树按 Retained Heap 排序。Dominator Tree 的核心概念A 支配 B 表示从 GC Roots 到 B 的所有路径都经过 A因此 A 的 Retained Heap 等于 A 被回收后能一并释放的内存总量。该视图可快速定位最大的垃圾源头。Step 4定位具体对象在 Dominator Tree 中找到 Retained Heap 最大的对象右键选择 “Path to GC Roots”再选择 “exclude weak/soft references”即可看到完整引用链。例如HashMap$Node → ArrayList → SomeService.staticList表明是静态集合不断 add 导致的内存泄漏。Step 5分析代码根因常见泄漏场景静态 Map 不断 put缓存未设置过期时间或未设置容量上限ThreadLocal 未 remove线程池复用线程Value 永远不会被释放监听器/回调未注销Observer 模式中遗漏 removeListener 调用数据库连接未关闭连接池中的连接持有大量结果集未释放内部类持有外部引用非静态内部类隐式持有Outer.this引用Step 6修复验证修复代码后进行灰度发布观察老年代在 GC 后的使用率是否趋于稳定。Step 7建立预防机制配置 JVM 参数-XX:HeapDumpOnOutOfMemoryError、设置老年代使用率超过 80% 的监控告警、定期 Review 静态集合和 ThreadLocal 的使用情况。四、死锁排查症状部分接口超时无响应其他接口正常线程池活跃线程数持续升高CPU 使用率不高线程均在阻塞等待而非自旋。# 使用 jstack dump 线程快照jstack PID/tmp/jstack.log# 查看文件末尾JVM 会自动检测死锁grepdeadlockjstack.log-i-A20jstack 输出的底部会直接打印死锁信息Found one Java-level deadlock:以及具体的线程和锁的持有/等待关系。常见死锁原因嵌套锁获取顺序不一致线程 A 先锁 a 再锁 b线程 B 先锁 b 再锁 a环状等待链线程 A 持有锁 A 等待锁 B线程 B 持有锁 B 等待锁 C线程 C 持有锁 C 等待锁 A数据库死锁两个事务以不同顺序更新同一组数据行避免策略统一加锁顺序使用tryLock(timeout)替代synchronized——超时后释放已持有锁并重试减小锁粒度缩短锁持有时间优先使用无锁数据结构ConcurrentHashMap、AtomicInteger替代显式加锁五、连接池耗尽一个慢 SQL 的雪崩链场景还原某个慢 SQL 占用了大量连接执行时间超过 30s后续请求因无法获取连接而阻塞等待连接池被占满如 Druid 默认 maxActive20线程池队列随之饱和触发拒绝策略最终导致大面积超时。这就是典型的雪崩效应——一个慢 SQL 拖垮整个应用。排查步骤# Arthas dashboard 查看线程状态——大量线程 BLOCKED 在 Druid 获取连接处# Druid 监控页面查看活跃连接数和等待队列长度# 定位持有连接时间最长的线程极可能是慢 SQL 的源头解决方案级别措施紧急手动 kill 慢 SQLSHOW PROCESSLIST定位后执行KILL ID释放连接短期设置 SQL 超时DruidqueryTimeout10s超时自动断开并释放连接长期慢 SQL 优化加索引/改写 SQL/加缓存连接池隔离核心与非核心业务使用不同数据源连接池参数调优六、Full GC 频繁调优现象线上每隔几分钟触发一次 Full GC每次 STW 持续 3-5 秒。排查步骤分析 GC 日志——-Xlog:gc*:file/tmp/gc.logJDK 9使用 GCeasy 或 GCViewer 工具分析。重点观察每次 Young GC 后晋升到老年代的对象数量Promotion Size、老年代何时被填满从而触发 Full GC。常见原因及优化方向原因现象优化年轻代太小对象未经过足够轮次就晋升老年代调大-Xmn设为总堆的 1/3 到 1/2Survivor 太小Minor GC 时对象直接进入老年代调小-XX:SurvivorRatio增大 Survivor 占比大对象直接进老年代GC 日志中出现 “humongous allocation”代码层面避免大对象或调大-XX:PretenureSizeThreshold老年代太小正常晋升也频繁触发 Full GC调大-Xmx但需先排查以上三项确认非内存泄漏MetaSpace 满GC 日志中出现 “Metadata GC Threshold”调大-XX:MaxMetaspaceSize内存泄漏GC 后老年代使用率持续上升参照第三章流程排查泄漏实战案例某次压测发现每 2 分钟一次 Full GCSTW 4sGC 日志显示每次 Young GCEden 200MB后约有 80MB 对象晋升老年代。分析发现 Survivor 区仅 20MB根本容纳不下存活对象导致对象直接进入老年代。调整方案SurvivorRatio 从 8 调整为 3Eden:S0:S13:1:1Survivor 扩大至 50MB晋升量降至约 10MB/次Full GC 消失。七、性能排查六步法从现象到根因、从宏观到微观的标准化排查流程Step 1确认问题范围1 分钟先回答三个问题是所有接口都慢还是仅某个接口慢全局 vs 局部是一直慢还是某个时间点突然变慢持续 vs 突发慢在哪个环节通过 SkyWalking / Zipkin 查看调用链各 Span 的耗时全局慢大概率是 JVM GC / 连接池 / 机器资源问题。局部慢大概率是某个 SQL / 下游调用 / 代码逻辑问题。Step 2定位瓶颈层级5 分钟用户请求 → 网关(Nginx) → 应用(Spring) → 业务代码 → SQL/Redis/RPC │ │ │ │ ↓ ↓ ↓ ↓ 超时配置 线程池/GC Arthas trace EXPLAIN/SlowLog 连接数 内存 Watch 观察 Redis 慢日志Step 3应用层排查Arthas 三板斧dashboard# 一眼看全局GC 频率、堆使用率、线程数、CPUthread-n10# 定位 CPU 最高的线程排查死循环或自旋trace ServiceImpl method-n20# 追踪方法调用链精确到每一步的耗时Step 4数据库层排查若 trace 指向 SQL按以下顺序分析EXPLAIN 查看执行计划type 是否为 ALL全表扫描、key 使用了哪个索引、rows 扫描行数是否合理查看慢查询日志通过SHOW PROFILES查看具体哪个阶段慢Sending data / Creating tmp table / Sorting锁分析通过SHOW ENGINE INNODB STATUS排查锁等待和死锁连接池通过 Druid / HikariCP 监控面板查看活跃连接数、等待队列、平均执行时间Step 5缓存/中间件层排查RedisSLOWLOG GET 20查看近期慢命令关注大 keyMEMORY USAGE key和热 keyMQ查看消费者积压的 lag 值积压过多时增加消费者实例RPC检查超时配置是否合理、熔断器是否已打开Step 6验证修复效果修复后必须有对比数据来证明效果。观察窗口至少持续 1 小时以排除缓存预热、JIT 编译等干扰因素。八、慢 SQL 优化实战标准流程开启慢查询日志long_query_time1smin_examined_row_limit1000EXPLAIN 分析执行计划重点关注typeALL 最差至少应达到 range、key是否命中索引、rows预估扫描行数、ExtraUsing filesort 表示需排序优化、Using temporary 表示使用了临时表常见优化手段建立合适的索引联合索引遵循最左前缀原则删除无用索引无用索引会拖累写性能改写 SQL用 JOIN 替代子查询、用 UNION ALL 替代 OR、避免SELECT *、避免在 WHERE 中对列使用函数大偏移量分页采用先查 ID 再关联策略大表考虑分库分表或同步至 ES实战案例Oracle 迁移 OceanBase 后查询从 2s 变为 5min现象某客户交割查询Oracle 上执行 2 秒OceanBase 上执行 5 分钟后超时排查过程拆分 SQL 逐块测试每次去掉一个条件测量耗时差异发现瓶颈由三部分组成PG 函数最大头约 80% 的开销 spotTrade NOT EXISTS 子查询 swapTrade/deals NOT EXISTS 子查询根因OceanBase 优化器对 PL/SQL 函数在 WHERE 中的处理方式为逐行调用——表有 410 万行两个子查询各扫一遍PG 函数每行都调用一次。410 万行乘以 2 次全量扫描再乘以每次函数查参数表导致时间爆炸式增长最终策略在 Java 层面预计算日期参数避免在 SQL 条件侧调用函数通用经验大表超过 100 万行加上 WHERE 中对列使用函数调用几乎必然导致性能灾难。函数务必放在变量侧预计算不可放在条件侧。数据库迁移不能仅验证功能正确性必须使用真实数据量进行性能回归测试。实战案例接口从 200ms 逐渐变慢至 3s现象某查询接口的响应时间随时间推移从 200ms 逐步增长至 3s重启后恢复正常之后再次逐步恶化排查通过 jstat -gcutil 持续观察发现老年代使用率持续上升Full GC 后仅回落极少部分。执行 heap dump 后使用 MAT Dominator Tree 分析发现一个静态 Map 中堆积了 50 万条数据每次请求都向其中 put 数据但从不清理根因缓存 Map 配置了Cacheable但未设置过期时间和最大容量限制修复改用 Caffeine CachemaximumSize10000, expireAfterWrite30min问题彻底解决九、性能优化的五层优先级按 ROI 从高到低排列业务优化减少无效请求 缓存避免查数据库 索引优化查得更快 代码优化算得更快 硬件加机器 ROI 最高 效果最明显 常用手段 最后手段第一优先级业务层优化ROI 最高、风险最低减少不必要的查询将循环中逐条查数据库改为批量查询将反复查部门名称改为一次 JOIN减少数据传输量将 10 万行全量返回前端改为后端分页将SELECT *改为仅查询需要的列异步化下单后的短信/邮件/推送通知通过消息队列异步处理削峰填谷秒杀场景使用 MQ 或令牌桶限流第二优先级缓存层浏览器缓存 → CDN 缓存 → Nginx 缓存 → Redis 缓存 → Caffeine 本地缓存三板斧最大容量防 OOM 过期时间防脏数据 布隆过滤器防穿透第三优先级数据库层索引优化 → SQL 改写避免 WHERE 中对列使用函数用 UNION ALL 替代 OR→ 读写分离 → 分库分表 → ES 同步第四优先级代码层CompletableFuture 并行调用替代串行 → 连接池调优 → 序列化优化Protobuf 替代 JSON→ 数据结构选择频繁 contains 操作使用 Set 而非 List第五优先级JVM 与基础设施GC 调优 → 硬件升级SSD、增加内存→ 水平扩展增加实例 负载均衡总结分层定位、逐层排除是线上问题排查的核心思维——从链路追踪SkyWalking/Zipkin确定慢在哪个 Span到 Arthas trace 树状输出方法调用链的每一步耗时再到数据库层的 EXPLAIN 分析和缓存层的 Redis 慢日志。三个最高频的生产事故各有标准排查路径CPU 飙高通过top -Hp PID定位线程后结合jstack定位代码行或 Arthasthread -n 5一步到位内存泄漏走jmap -dump→ MAT Dominator Tree按 Retained Heap 排序→ Path to GC Roots 的完整链路其中静态集合无限增长和 ThreadLocal 未 remove 是最常见的泄漏来源慢 SQL用 EXPLAIN 关注 type/key/rows/Extra 四列联合索引遵循最左前缀原则避免在 WHERE 中对列使用函数。连接池耗尽是最典型的雪崩场景——一个慢 SQL 占用连接 → 连接池满 → 线程阻塞等待 → 线程池饱和 → 拒绝策略触发 → 大面积超时。紧急止血用KILL ID长期方案靠 SQL 超时机制加连接池隔离。性能优化的优先级按 ROI 排序为业务优化减少无效请求 缓存避免查数据库 索引优化查得更快 代码优化算得更快 硬件加机器核心能力是根据现象判断哪种手段的 ROI 最高。系列专栏《Java 后端核心知识图谱》172 篇Java专栏下一篇《Redis 核心原理六大数据结构 RDB/AOF双引擎 缓存一致性方案》配套代码PrismAI如果本文帮你省下了一次半夜被报警叫醒排查的时间欢迎收藏 点赞