本文目录导读:

Java故障报告案例通常涉及以下几个方面:内存溢出(OOM)、CPU飙升、线程死锁、频繁GC(垃圾回收)以及连接池耗尽。
以下是几个典型的故障报告案例总结,并附带问题排查思路和解决方案,供参考:
内存溢出(OOM)— 堆内存泄漏
故障现象:
某后台服务运行一周后,接口响应越来越慢,最终抛出 java.lang.OutOfMemoryError: Java heap space,服务宕机。
故障定位过程:
- 日志分析:查看启动参数为
-Xmx2g,该服务为并发不高但数据量较大的批处理任务。 - MAT分析:用
jmap -dump:format=b,file=heap.hprof <pid>导出堆快照,用Eclipse MAT分析发现,java.util.ArrayList占据了80%的内存,且对象持有者指向自定义类BatchDataHolder。 - 代码定位:
// 伪代码 public void processBatch() { List<Data> list = new ArrayList<>(); for (Data d : queryAllData()) { // 一次性加载了所有数据到内存 list.add(d); } // 处理逻辑省略... }发现代码将数据库全表数据一次性查询并放入List中,且该List被静态变量持有,无法被释放。
解决方案:
- 修改代码为分页查询或流式处理(如使用
Cursor)。 - 将静态变量持有改为局部变量,处理完成后设为
null。 - 适当调大
-Xmx参数(治标不治本)。
CPU飙升 100%
故障现象: 告警平台提示某Java服务 CPU 使用率持续 99%,但业务量并未增长。
故障定位过程:
- 定位线程:
top -Hp <pid>找到 CPU 最高的线程 PID(如 12345)。 - 转换PID:
printf "%x\n" 12345转为十六进制(0x3039)。 - 打印堆栈:
jstack <pid> | grep "0x3039" -A 50,定位到main线程或某业务线程。 - 代码定位:发现某
while循环中有一个Thread.sleep(0)的写法,且没有yield,导致自旋锁占用CPU。
// 错误代码示例
while (true) {
if (queue.isEmpty()) {
Thread.sleep(0); // 这里sleep(0)不会让出CPU时间片,导致忙等待
}
// 处理任务...
}
解决方案:
- 改为
Thread.sleep(100)或使用阻塞队列BlockingQueue.take()。 - 优化算法,避免无意义的循环计算。
数据库连接池耗尽
故障现象:
系统出现偶发性超时,日志频繁报错 Connection is not available, request timed out after 30000ms(HikariCP)。
故障定位过程:
- 查看连接池配置:
maximum-pool-size: 50。 - 查看Java线程栈:
jstack发现大量线程阻塞在java.sql.Connection的获取方法上(等待连接)。 - 数据库侧排查:
show processlist;发现大量Sleep状态的连接。 - 代码定位:FIND BUG 发现存在某处
finally块中未关闭 Connection,或者使用了@Transactional内部调用this.method()导致事务未走代理,连接未释放。
解决方案:
- 规范代码,确保在
finally中释放连接(或使用try-with-resources)。 - 排查
@Transactional的失效场景(如内部方法调用),强制使用代理对象注入。 - 调整连接池超时时间,增加
connection-timeout监测。
Full GC 频繁导致系统卡顿
故障现象:
应用吞吐量下降,STW(Stop-The-World)时间过长,监控平台显示 Full GC 高达每秒一次。
故障定位过程:
- 查看GC日志:启动参数加
-XX:+PrintGCDetails -XX:+PrintGCDateStamps。 - 分析GC日志(使用
GCeasy或GCViewer):老年代(Old Gen)持续增长且回收后内存不降。 - 堆转储:发现大量
byte[]对象持有,对应到逻辑是未释放的 Response 缓存或大 List 缓存。 - 根本原因:代码中使用了
ConcurrentHashMap作为缓存,但未设置过期时间或上限,导致缓存不断膨胀直至触发老年代OOM或频繁Full GC。
解决方案:
- 引入本地缓存框架(如 Caffeine、Guava Cache)并设置合理的过期策略(TTL/LRU)。
- 调整堆内存比例(
-XX:NewRatio),增大新生代避免大对象直接进入老年代。 - 优化业务逻辑,减少大对象的创建。
多线程死锁
故障现象:
服务有部分请求持续超时,jstack 检测到 Found one Java-level deadlock。
故障定位过程:
- jstack 输出:显示线程 A 持有 Lock1 等待 Lock2,线程 B 持有 Lock2 等待 Lock1。
- 源代码定位:两个Service方法交叉申请锁,且锁顺序不一致导致了死锁。
// 错误代码示例
public void transferMoney(Account a, Account b, double amount) {
synchronized (a) {
synchronized (b) {
// 转账逻辑
}
}
}
// 两个线程分别调用 transferMoney(a,b) 和 transferMoney(b,a) 导致死锁
解决方案:
- 统一锁的顺序(如按账户ID排序后再加锁)。
- 使用
ReentrantLock的tryLock()或lockInterruptibly()设置超时,避免永久阻塞。 - 将锁粒度细化,降低锁持有时间。
故障报告标准模板(通用)
一个好的故障报告通常包含以下结构,你可以直接套用:
| 字段 | |
|---|---|
| 故障等级 | P0(紧急)/ P1(严重)/ P2(一般) |
| 影响范围 | 影响哪些业务/用户,是否数据丢失 |
| 发生时间 | 具体时间点及持续时间 |
| 根因类别 | 代码缺陷 / 配置不当 / 硬件故障 / 第三方依赖 / 流量突增 |
| 表面现象 | 报错日志信息、监控图表(CPU/内存/GC) |
| 处理过程 | 回滚 2. 重启 3. 临时扩容 4. 详细定位代码逻辑 |
| 根本原因 | 具体到某行代码或某个配置项 |
| 优化措施 | 代码反思、监控告警补充(如增加堆内存监控)、自动化测试防护 |
总结建议
在遇到Java故障时,优先检查:
- 日志(排查异常栈)。
- 内存和GC(
jstat -gcutil)。 - 线程状态(
jstack)。 - SQL/连接池(查看数据库慢查询和活动连接数)。
永远不要重启来解决故障,如果必须要重启,请先用 jmap / jstack 保留现场证据,否则无法定位根因。