Java线上诊断案例

wen java案例 2

本文目录导读:

Java线上诊断案例

  1. 案例一:CPU 100% 飙升(死循环/正则回溯)
  2. 案例二:内存溢出(OOM)—— 线程创建过多导致无法分配栈内存
  3. 案例三:应用假死、端口不响应(死锁)
  4. 案例四:GC 频繁导致 CPU 高(内存泄漏初期)
  5. 案例五:连接数打满(数据库连接池 / HTTP连接池)
  6. 线上诊断三部曲
  7. 附录:诊断神兵利器 Arthas

Java线上诊断是每个高级开发必备的技能,下面整理了几个真实的经典案例,涵盖CPU飙升、内存泄漏、线程阻塞、以及GC频繁问题,并提供完整的排查思路与命令。


CPU 100% 飙升(死循环/正则回溯)

症状:线上告警,某台机器CPU使用率飙升至100%,接口响应变慢,但服务并未宕机。

排查思路进程 -> 线程 -> 线程栈

操作步骤:

  1. 定位进程(如果是多实例,需要找到具体的那台机器):

    top -c

    查看CPU占用最高的PID(假设为 12345)。

  2. 定位线程(找到进程里最耗CPU的线程ID):

    top -Hp 12345

    找到CPU极高的线程PID(假设为 12346)。

  3. 转换线程ID为十六进制

    printf "%x\n" 12346
    # 输出: 303a
  4. 查看线程栈(导出快照):

    jstack 12345 | grep -A 30 "0x303a"

真实诊断jstack 输出显示线程卡在 java.util.regex.Pattern$Curly.matchPattern$GroupHead.match

根因:代码中使用了类似 (a|aa)+b 这样的正则表达式,对长字符串进行匹配,触发了灾难性回溯(Catastrophic Backtracking),导致CPU空转。

解决方案

  • 临时方案:重启实例或紧急发布回滚。
  • 修复方案
    1. 优化正则表达式,消除嵌套量词(如 (a+)+)。
    2. 引入RE2/J等线性时间正则引擎。
    3. 如果必须使用Java正则,增加超时熔断机制。

内存溢出(OOM)—— 线程创建过多导致无法分配栈内存

症状:服务突然宕机,日志中无具体业务报错,系统日志(dmesg)报错。

排查思路系统日志 -> JVM参数 -> 创建线程的代码

操作步骤

  1. 查看系统日志:

    dmesg | tail -20
    # 报错: java: page allocation failure: order:0, mode:0x0
    # 或者: Out of memory: Kill process 12345 (java) score 989

    注意:如果是操作系统层面的OOM Kill,JVM日志里可能没有OutOfMemoryError堆栈。

  2. 查看JVM进程的线程数:

    cat /proc/12345/status | grep Threads
    # 显示: Threads: 5000
    # 发现线程数异常高(正常情况下几百以内)
  3. 打印线程快照,统计线程状态:

    jstack 12345 | grep "java.lang.Thread.State" | sort | uniq -c | sort -rn
    # 发现大量: WAITING (parking)
  4. 查看磁盘空间

    df -h
    # 查看 /tmp 或日志目录是否写满,导致日志组件内部创建线程失败

根因:业务代码中存在死循环创建线程,或者使用了Executors.newCachedThreadPool()且未限制最大线程数,导致线程无限创建,最终撑爆内存或被OS杀死。

解决方案

  • 使用ThreadPoolExecutor并显式指定有界队列和最大线程数。
  • 排查为什么线程数会无限增长(可能是下游接口超时,导致任务堆积在队列外)。

应用假死、端口不响应(死锁)

症状:HTTP接口长时间无响应,但进程还在,CPU占用率很低(甚至为0%)。

排查思路线程Dump -> 寻找 BLOCKED 线程 -> 寻找锁

操作步骤

  1. 执行 jstack,注意需要多执行几次(间隔几秒),抓取2-3份快照。

    jstack 12345 > /tmp/jstack1.txt
    sleep 3
    jstack 12345 > /tmp/jstack2.txt
  2. 检查死锁(jstack 自带检测): 在输出的最后面,通常会打印 Found one Java-level deadlock: 字样。

  3. 如果没有打印,需要手动分析:

    grep -B 5 -A 10 "locked" /tmp/jstack1.txt

    寻找拥有锁但处于 WAITING 的线程,以及等待该锁的 BLOCKED 线程。

真实案例:两个线程互相持有对方需要的锁:

  • Thread-A 持有 Lock-X,等待 Lock-Y
  • Thread-B 持有 Lock-Y,等待 Lock-X

定位代码:检查是哪个类中的 synchronizedReentrantLock

解决方案

  • 临时:重启服务。
  • 修复:调整加锁顺序,统一锁的获取顺序;使用 tryLock(long timeout, TimeUnit unit) 代替直接 lock(),避免无限期阻塞。

GC 频繁导致 CPU 高(内存泄漏初期)

症状:CPU使用率不高(20%-30%),但接口RT急剧上升,Full GC 频繁。

排查思路堆内存分析 -> GC日志 -> MAT分析

操作步骤

  1. 查看GC日志(如果JVM启动时未开启,需要加上重启):

    jstat -gcutil 12345 1000 10
    # 每隔1秒打印一次,连续10次
    # 观察 FGC (Full GC次数) 是否快速增长,FGCT (时间) 是否很长

    输出示例:

    S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
    0.00   0.00  45.00  98.00  95.00  97.00   500   10.0    100    20.0   30.0

    可以看到 Old区(O)占用98%,FGC已达100次。

  2. 导出堆转储(需要一定时间,最好在低峰期操作):

    jmap -dump:live,format=b,file=/tmp/heap.hprof 12345
    # 注意: 生产环境请谨慎使用,会暂停应用(STW)
  3. 使用MAT (Memory Analyzer Tool) 分析

    • 打开 heap.hprof
    • 查看 Leak Suspects Report
    • 查看 Dominator Tree 查找最大的对象。

真实案例:分析发现 java.util.HashMap 占用了 80% 的堆内存。

根因定位:代码中使用 ThreadLocal 存储了用户信息,但请求结束后未执行 remove(),由于线程池中的线程复用,导致 ThreadLocalMap 中的 Entry 强引用一直在扩容,导致内存持久增长,最终引发频繁FullGC。

解决方案

  • 使用完 ThreadLocal 后,在 finally 块中调用 remove()
  • 如果使用 Netty 等框架,注意其 FastThreadLocal 的清理机制。

连接数打满(数据库连接池 / HTTP连接池)

症状:服务日志大量报错:Connection is not available, request timed out after 30000ms

排查思路连接池监控 -> 慢SQL -> 未释放连接

操作步骤

  1. 查看连接池指标(如果接入了Prometheus+Grafana,可直接看图表)。

  2. 查看MySQL中的连接状态(如果是DB连接池):

    SHOW PROCESSLIST;
    -- 寻找大量处于 'Sleep' 状态的连接,且时间很长
  3. 代码排查: 检查 HikariCP / Druid 的配置,maximum-pool-size 是否过小?connection-timeout 是否过短?

  4. 分析Druid监控页面(如果是Druid),查看 ActiveCountPoolingCount

真实案例:业务代码使用了 RestTemplate 调用外部API,未设置连接池和超时时间,导致外部API变慢时,所有 HTTP 连接被占用,大量线程阻塞在获取连接上,进而导致数据库连接池被业务线程占满。

解决方案

  • 所有外部调用必须设置 connectTimeoutreadTimeout
  • 使用带连接池的 HTTP 客户端(OkHttp / Apache HttpClient),并合理设置最大连接数。
  • 排查是否有数据库查询逻辑忘记 close()(虽然 try-with-resources 已普及,但手动获取 Connection 的场景仍需注意)。

线上诊断三部曲

遇到问题不要慌,按以下顺序操作:

  1. 看现象

    • CPU高 -> 查线程(案例一)。
    • 内存高/GC频繁 -> 查堆(案例四)。
    • 无响应/死锁 -> 查线程锁状态(案例三)。
    • 依赖服务故障 -> 查连接池和超时(案例五)。
  2. 取快照

    • jstack - 线程快照。
    • jmap - 堆快照。
    • jstat - GC统计。
    • dmesg - 系统层日志。
  3. 深分析

    利用 MAT 分析大对象,利用 VisualVM 离线分析,或者用 Arthas(阿尔萨斯)在线热更新排查(推荐熟悉)。


附录:诊断神兵利器 Arthas

Arthas 是阿里开源的Java诊断工具,能解决上述很多需要重启才能解决的难题。

# 启动
java -jar arthas-boot.jar
# 在Arthas控制台内
# 1. 反编译线上代码,确认线上跑的是不是最新代码
jad com.example.MyService
# 2. 查看某个方法的入参和返回值(相当于动态打印日志)
watch com.example.MyService getUserInfo '{params[0], returnObj}' -x 3
# 3. 查看方法耗时和各步骤耗时(火焰图)
trace com.example.MyService getUserInfo
# 4. 查看线程CPU占用
thread -n 3

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