Java故障报告案例

wen java案例 4

本文目录导读:

Java故障报告案例

  1. 案例一:内存溢出(OOM)— 堆内存泄漏
  2. 案例二:CPU飙升 100%
  3. 案例三:数据库连接池耗尽
  4. 案例四:Full GC 频繁导致系统卡顿
  5. 案例五:多线程死锁
  6. 故障报告标准模板(通用)
  7. 总结建议

Java故障报告案例通常涉及以下几个方面:内存溢出(OOM)CPU飙升线程死锁频繁GC(垃圾回收)以及连接池耗尽

以下是几个典型的故障报告案例总结,并附带问题排查思路和解决方案,供参考:


内存溢出(OOM)— 堆内存泄漏

故障现象: 某后台服务运行一周后,接口响应越来越慢,最终抛出 java.lang.OutOfMemoryError: Java heap space,服务宕机。

故障定位过程:

  1. 日志分析:查看启动参数为 -Xmx2g,该服务为并发不高但数据量较大的批处理任务。
  2. MAT分析:用 jmap -dump:format=b,file=heap.hprof <pid> 导出堆快照,用Eclipse MAT分析发现,java.util.ArrayList 占据了80%的内存,且对象持有者指向自定义类 BatchDataHolder
  3. 代码定位
    // 伪代码
    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%,但业务量并未增长。

故障定位过程:

  1. 定位线程top -Hp <pid> 找到 CPU 最高的线程 PID(如 12345)。
  2. 转换PIDprintf "%x\n" 12345 转为十六进制(0x3039)。
  3. 打印堆栈jstack <pid> | grep "0x3039" -A 50,定位到 main 线程或某业务线程。
  4. 代码定位:发现某 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)。

故障定位过程:

  1. 查看连接池配置maximum-pool-size: 50
  2. 查看Java线程栈jstack 发现大量线程阻塞在 java.sql.Connection 的获取方法上(等待连接)。
  3. 数据库侧排查show processlist; 发现大量 Sleep 状态的连接。
  4. 代码定位:FIND BUG 发现存在某处 finally 块中未关闭 Connection,或者使用了 @Transactional 内部调用 this.method() 导致事务未走代理,连接未释放。

解决方案:

  • 规范代码,确保在 finally 中释放连接(或使用 try-with-resources)。
  • 排查 @Transactional 的失效场景(如内部方法调用),强制使用代理对象注入。
  • 调整连接池超时时间,增加 connection-timeout 监测。

Full GC 频繁导致系统卡顿

故障现象: 应用吞吐量下降,STW(Stop-The-World)时间过长,监控平台显示 Full GC 高达每秒一次。

故障定位过程:

  1. 查看GC日志:启动参数加 -XX:+PrintGCDetails -XX:+PrintGCDateStamps
  2. 分析GC日志(使用 GCeasyGCViewer):老年代(Old Gen)持续增长且回收后内存不降。
  3. 堆转储:发现大量 byte[] 对象持有,对应到逻辑是未释放的 Response 缓存大 List 缓存
  4. 根本原因:代码中使用了 ConcurrentHashMap 作为缓存,但未设置过期时间或上限,导致缓存不断膨胀直至触发老年代OOM或频繁Full GC。

解决方案:

  • 引入本地缓存框架(如 Caffeine、Guava Cache)并设置合理的过期策略(TTL/LRU)。
  • 调整堆内存比例(-XX:NewRatio),增大新生代避免大对象直接进入老年代。
  • 优化业务逻辑,减少大对象的创建。

多线程死锁

故障现象: 服务有部分请求持续超时,jstack 检测到 Found one Java-level deadlock

故障定位过程:

  1. jstack 输出:显示线程 A 持有 Lock1 等待 Lock2,线程 B 持有 Lock2 等待 Lock1。
  2. 源代码定位:两个Service方法交叉申请锁,且锁顺序不一致导致了死锁。
// 错误代码示例
public void transferMoney(Account a, Account b, double amount) {
    synchronized (a) {
        synchronized (b) {
            // 转账逻辑
        }
    }
}
// 两个线程分别调用 transferMoney(a,b) 和 transferMoney(b,a) 导致死锁

解决方案:

  • 统一锁的顺序(如按账户ID排序后再加锁)。
  • 使用 ReentrantLocktryLock()lockInterruptibly() 设置超时,避免永久阻塞。
  • 将锁粒度细化,降低锁持有时间。

故障报告标准模板(通用)

一个好的故障报告通常包含以下结构,你可以直接套用:

字段
故障等级 P0(紧急)/ P1(严重)/ P2(一般)
影响范围 影响哪些业务/用户,是否数据丢失
发生时间 具体时间点及持续时间
根因类别 代码缺陷 / 配置不当 / 硬件故障 / 第三方依赖 / 流量突增
表面现象 报错日志信息、监控图表(CPU/内存/GC)
处理过程 回滚 2. 重启 3. 临时扩容 4. 详细定位代码逻辑
根本原因 具体到某行代码或某个配置项
优化措施 代码反思、监控告警补充(如增加堆内存监控)、自动化测试防护

总结建议

在遇到Java故障时,优先检查:

  1. 日志(排查异常栈)。
  2. 内存和GCjstat -gcutil)。
  3. 线程状态jstack)。
  4. SQL/连接池(查看数据库慢查询和活动连接数)。

永远不要重启来解决故障,如果必须要重启,请先用 jmap / jstack 保留现场证据,否则无法定位根因。

抱歉,评论功能暂时关闭!