Java 服务最常见的线上性能故障

一、内存相关(最常见、最致命)

1. 内存泄漏(Memory Leak)

  • 现象堆内存只涨不跌,Full GC 后不下降,最终 OOM
  • 根本原因:对象被长期持有(static 集合、ThreadLocal、线程池、连接未释放)
  • 后果:服务卡死、重启才能恢复
  • 判断方法
    • jstat -gc 看到:老年代持续上涨,Full GC 后内存不下降
    • 监控曲线:堆内存只增不减
    • 最终抛出:OOM: Java heap space
  • 定位泄露代码:
    • 导出堆快照:# 自动导出(推荐,OOM 时自动 dump,不影响运行) -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
    • MAT分析
    • 看引用链
  • 解决方法
    • 打断引用链:手动设置null、调用 remove/clear/close
    • 加自动清理机制:过期时间、最大容量、淘汰策略
    • 使用安全工具类:try-with-resources、规范线程池
    • 截止无界缓存、无界集合
  • 应急方法
    • 重启服务
    • dump命令输出内存信息
    • 修复、上线
  • 预防机制
    • 代码规范:ThreadLocal 必 remove连接必关闭
    • 禁止使用 static 集合做本地缓存
    • 缓存必须用:Caffeine、Guava、Redis
    • 开启监控:堆内存、Full GC 次数
    • 代码 review 重点检查:缓存、线程池、IO

2. 内存溢出 OOM

  • 根本原因内存泄漏、大对象、堆设置太小
  • 常见的OOM
    • 堆溢出:Java heap space
    • GC 超时溢出 GC overhead limit exceeded
  • 解决方法
    • 禁止一次性加载大量数据:分页、分批、流式处理
    • 禁止无界缓存:不用 static Map 做缓存
    • 资源必须关闭
    • 合理使用线程池​​​​​​​​​​​​​​
1.2.1. 堆内存溢出
  • 现象:报错 java.lang.OutOfMemoryError: Java heap space

  • 原因

    • 一次查询全表数据(几十万、上百万条)

    • 内存泄漏耗光堆

    • 堆内存设置太小

    • 大对象(大 List、大文件、大报文)

  • 解决方法

    • 临时扩容(快速恢复)
    • 代码根治
      • SQL 必须分页查询 ,禁止 select * 全表加载
      • 修复内存泄漏(static 集合、ThreadLocal 等)
      • 大文件流式处理,不要一次性读入内存
    • 检查是否有超大对象、超大集合
      • 元空间溢出:Metaspace
      • 直接内存溢出
1.2.2. GC 超时溢出
  • 现象:GC 占用太多 CPU,回收不掉内存,直接抛 OOM

  • 原因:

    • 内存快耗尽,Full GC 疯狂跑

    • 99% 是内存泄漏

  • 解决方法:

    • 内存泄漏流程排查
    • 清理无用对象引用
    • 适当加大堆内存

3. GC 频繁 / Full GC 频繁(几分钟一次,甚至几十秒一次)

  • 现象:CPU 飙升、接口响应慢、TP99 暴涨

  • 根本原因:内存泄漏、新生代太小、大对象直接进入老年代

  • 判断方法 :jps # 找到进程 PID 、jstat -gc PID 1s # 实时看 GC

    • YGC 飙升 → 新生代问题
    • FGC 飙升 + 老年代涨内存泄漏 / 堆太小
    • FGC 后内存不下降100% 内存泄漏
  • 解决方法:

    • 急救(5 分钟恢复):重启服务、临时加大堆内存

    • 加 JVM 参数:

      • YGC 频繁 → 加大 -Xmn
      • FGC 频繁 → 加大堆 Xmx,或使用 G1
      • FGC 频繁 + 内存不下降 → 内存泄漏,必须改代码
      • 高并发接口 → 新生代给大一点(1/2 堆)
      • 业务对象多、缓存多 → 老年代给大一点
    • 定位根因

      • jstat 看 FGC 是否频繁

      • 有泄漏 → MAT 分析

      • 无泄漏 → 调大内存 + 优化代码

  • 代码根治:

    • 分页查询,禁止全表加载
    • ThreadLocal 必须 remove
    • 连接 / 流必须关闭
    • static 集合不能无限增长
    • 线程池必须用有界队列

二、CPU 相关(极易触发告警)

1. CPU 使用率过高(100%/80%+)

  • Top 原因及解决方法
    • Full GC 疯狂执行(占 70%):通过jstat -gc PID xxx 命令确认。 如果 FGC 疯狂上涨、秒秒都在 GCCPU 高 = GC 导致

      如果 GC 正常、YGC/FGC 都不高CPU 高 = 业务代码导致

    • 代码死循环:找到代码,加结束条件

    • 大量线程锁竞争:减少锁粒度、使用无锁结构、分段锁

    • 复杂计算 / 正则 / 序列化:优化正则、拆分、限制长度

  • 后果:服务不响应、雪崩
2.1.1. GC 导致 CPU 高(最常见)
  • 现象
    • jstat 看到 FGC 频繁上涨
    • GC 线程把 CPU 打满
  • 解决方法(直接做)
    • 加大堆内存
    • 按之前的 Full GC 频繁方案处理
    • 排查内存泄漏(导出 dump,MAT 分析)
2.1.2. 业务代码导致 CPU 高(真正要查代码)
  • 排查方法:
    • 找到耗 CPU 的 Java 进程 ------ top命令找到PID
    • 找到进程内最耗 CPU 的线程 ------ top -Hp PID拿到线程TID
    • 把线程 ID 转成 16 进制 ------ printf "%x\n" TID
    • 导出线程栈,定位代码行 ------ jstack 16进制结果 > cpu.log

2. 死锁 / 锁竞争激烈

  • 现象:接口卡住、线程大量 WAITING
  • 根因:synchronized 嵌套、ReentrantLock 未释放、线程池设计不合理
  • 定位:
    • 直接查死锁(自动检测)
      • jstack -l PID > lock.log
      • 打开日志,搜索关键字 Found 1 deadlock
    • 看锁竞争:
      • jstack PID | grep -C 10 "WAITING"
      • 大量线程 WAITING 且都等同一个锁地址→ 锁竞争激烈
  • 解决方法:
    • 统一锁的获取顺序
    • 使用定时锁,避免无限等待
    • 减少嵌套锁(不要锁中锁)
    • 无锁设计(ThreadLocal、CAS)
    • 加锁粒度拆小
低效锁 推荐高效锁
synchronized 全局锁 ReentrantLock
普通 HashMap ConcurrentHashMap
自己写的锁 LongAdder(计数)
独占锁 读写锁 ReadWriteLock

三、接口 / 业务性能(用户感知最强)

1. 接口响应慢 / TP99 飙升

  • 最常见原因
    • 慢 SQL(没索引、连表太深)
    • 外部接口调用超时
    • Redis 缓存失效 / 击穿
    • 锁等待、GC 停顿
    • 大事务、长事务
  • 定位:
    • 看监控(链路追踪):Arthas看 **哪一段耗时最高?**是 DB?HTTP?Redis?还是业务代码?
    • 抓慢接口日志(无监控也能查):grep -r "接口名" app.log | grep "耗时";偶尔慢 (GC / 锁),还是一直慢(SQL / 外部调用)
    • Arthas 一键定位:
      • 查看方法耗时 ------ trace 类名 方法名

      • 查看慢调用 ------ watch 类名 方法名 '{params,returnObj,throwExp}' -n 5 -x 3 'cost>100'

3.1.1. 慢 SQL / DB 慢(最常见)

  • 现象:
    • 接口 TP99 突然飙升
    • 数据库 CPU 高、连接池等待
    • 日志里 SQL 执行几十~几百毫秒
  • 解决方法:加索引、分页查询、大SQL拆分、加缓存

3.1.2. 外部接口调用慢(HTTP/Dubbo)

  • 现象:
    • 调用第三方服务、支付、短信、其他微服务
    • 超时、响应慢、不稳定
  • 解决方法:加超时时间、熔断降级、异步化、加缓存

3.1.3. Redis 问题

  • 现象:
    • 缓存击穿、缓存雪崩
    • Redis 命令慢
    • 高并发下缓存失效,请求打到 DB
  • 解决方法:
    • 缓存穿透 / 击穿:加布隆过滤器、空值缓存
    • 缓存雪崩:过期时间加随机值、集群部署
    • 大 Key 优化:避免超大 Hash、List

2. 线程池耗尽 / 异步任务堆积

  • 因果关系:
    • 任务堆积 → 处理不过来
    • 队列满了 + 最大线程数也满了线程池耗尽
    • 报错RejectedExecutionException
  • 根因:线程池参数不合理、任务阻塞、消费不过来
  • 直观判断:
    • 日志大量抛:拒绝执行异常
    • 异步任务不执行、延迟极高
    • 接口被拖慢、雪崩
  • 定位:arthas-boot.jar threadpool看到 活动线程数、队列堆积数、任务执行耗时
  • 解决方法
    • 调整队列大小:建议200~500 根据并发调整
    • 优化线程池参数:
      • CPU 密集型 (计算):最大线程 = CPU 核数 + 1
      • IO 密集型 (HTTP、DB、Redis):最大线程 = 50~200
    • 更换拒绝策略:推荐:CallerRunsPolicy ​​​​​​​​​​
      • 队列满了 → 让调用者自己执行
      • 不会丢任务、不会雪崩
      • 相当于自动限流
    • 优化任务执行速度:
      • SQL 慢 → 加索引
      • HTTP 调用慢 → 加超时、熔断
      • 锁等待 → 缩粒度、换无锁
      • 外部调用慢 → 异步化、并行化
    • 终极解决方案(高并发):异步任务太多 → 用 MQ 削峰 (最稳)
      1. 任务不直接扔线程池
      2. 发送到 RabbitMQ / RocketMQ / Kafka
      3. 消费者慢慢消费彻底解决堆积、耗尽、雪崩

四、连接 / 资源泄漏(隐形杀手)

1. 连接泄漏(DB/Redis/HTTP)

  • 现象:运行一段时间后无法获取连接
  • 根因:连接获取后未关闭、try-with-resources (Java 7+ 专门用来解决资源泄漏)未使用

2. 文件句柄 / 流未关闭

  • 现象:导致 Too many open files
  • 结果:服务崩溃无法重启
  • 解决:使用 try-with-resources 所有流、文件、连接自动关闭
相关推荐
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章37-区域截图
图像处理·人工智能·opencv·算法·计算机视觉
96772 小时前
Java 类映射数据库表的核心规则
java·数据库·oracle
DeepModel2 小时前
【概率分布】正态分布(高斯分布)原理、可视化与机器学习实战
python·算法·概率论
啊哦呃咦唔鱼2 小时前
LeetCode hot100-239 滑动窗口最大值
数据结构·算法·leetcode
阳光下的米雪2 小时前
存储过程的使用以及介绍
java·服务器·数据库·pgsql
yoyo_zzm2 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
tuyanfei2 小时前
Spring 简介
java·后端·spring
m0_743297422 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
遥遥晚风点点2 小时前
JAVA http请求报错:unable to find valid certification path to requested target
java·网络·网络协议·http