作为一名Java开发者,线上环境故障排查是必备的核心技能。与本地开发环境不同,线上环境面临更复杂的网络条件、更高的并发压力和更严苛的资源限制。本文将分享我在处理Java线上故障时的实战经验,涵盖常见问题场景、排查思路和实用工具链。
线上环境与本地开发环境存在几个关键区别:
理解这些差异是有效排查线上问题的前提。下面我们来看几个典型故障场景。
当发现"昨天还能用,今天突然挂掉"的接口时,建议按以下优先级排查:
网络连通性检查
bash复制ping 目标服务IP
telnet IP 端口
traceroute IP
网络问题是最高频的故障原因,特别是跨机房服务调用。
服务器基础资源检查
bash复制df -h # 磁盘空间
free -m # 内存使用
top # CPU负载
磁盘满会导致日志无法写入,进而阻塞业务线程。
JVM状态检查
bash复制jps -l # 查看Java进程
jstat -gcutil PID 1000 # 实时GC监控
GC频繁或Old区持续增长都可能是问题征兆。
针对不同问题类型,需要使用专业工具:
| 问题类型 | 排查工具 | 关键指标 |
|---|---|---|
| CPU飙高 | top + jstack | 线程CPU占用率 |
| 内存泄漏 | jmap + MAT | 对象保留大小/引用链 |
| 死锁 | jstack | "deadlock"关键词 |
| 慢SQL | 数据库慢查询日志 | 执行时间>500ms的查询 |
| 线程阻塞 | arthas thread -b | BLOCKED状态的线程 |
提示:生产环境建议使用arthas代替jstack,可以避免频繁dump影响服务性能
一个典型的死锁需要同时满足四个条件:
以下是两个典型的死锁实现方式:
版本1:同步块嵌套
java复制public void deadLock1() {
final Object lockA = new Object();
final Object lockB = new Object();
new Thread(() -> {
synchronized(lockA) {
sleep(100); // 确保死锁发生
synchronized(lockB) {
System.out.println("Thread1 got both locks");
}
}
}).start();
new Thread(() -> {
synchronized(lockB) {
sleep(100);
synchronized(lockA) {
System.out.println("Thread2 got both locks");
}
}
}).start();
}
版本2:等待/通知机制
java复制public void deadLock2() {
final Object lock = new Object();
new Thread(() -> {
synchronized(lock) {
try {
lock.wait(); // 释放锁并等待
} catch (Exception e) {}
}
}).start();
sleep(100); // 确保线程进入等待
synchronized(lock) {
lock.notify(); // 无法唤醒,因为当前线程持有锁
}
}
使用jstack排查死锁的步骤:
bash复制jps -l
bash复制jstack -l PID > thread.log
典型死锁日志特征:
code复制Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f88e4003e58 (object 0x000000076ab45c80)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f88e4003f98 (object 0x000000076ab45c90)
which is held by "Thread-1"
定位高CPU进程
bash复制top -c
按P按CPU使用率排序
定位问题线程
bash复制top -Hp PID
记录高CPU线程ID(十进制)
线程ID转换
bash复制printf "%x\n" 线程ID
转换为十六进制用于jstack查找
分析线程栈
bash复制jstack PID | grep -A 20 十六进制线程ID
| 原因类型 | 特征 | 解决方案 |
|---|---|---|
| 死循环 | 同一方法持续占用CPU | 修复循环条件 |
| 频繁GC | GC线程占用高 | 调整JVM参数/修复内存泄漏 |
| 锁竞争 | 大量线程BLOCKED状态 | 优化锁粒度/改用并发容器 |
| 算法效率低 | 复杂运算持续占用CPU | 优化算法/引入缓存 |
| 外部攻击 | 异常IP大量请求 | 接入WAF/限流 |
实时监控
bash复制jstat -gcutil PID 1000
关注Old区使用率和Full GC频率
堆转储分析
bash复制jmap -dump:live,format=b,file=heap.hprof PID
使用MAT/Eclipse Memory Analyzer分析
OOM自动转储
JVM参数配置:
code复制-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
静态集合滥用
java复制public class LeakDemo {
static List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 对象永远无法释放
}
}
未关闭资源
java复制public void readFile() {
InputStream is = new FileInputStream("large.txt");
// 忘记调用is.close()
}
监听器未注销
java复制eventBus.register(listener);
// 忘记unregister
ThreadLocal滥用
java复制ThreadLocal<BigObject> tl = new ThreadLocal<>();
tl.set(new BigObject());
// 线程复用时不清理
现象:
排查命令:
sql复制SHOW STATUS LIKE 'Threads_connected';
SHOW PROCESSLIST;
解决方案:
定位方法:
开启慢查询日志
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
使用explain分析
sql复制EXPLAIN SELECT * FROM large_table WHERE unindexed_column = 'value';
常见优化手段:
完善的监控体系:
日志规范:
压测与预案:
Arthas:在线诊断神器
bash复制thread -b # 查看阻塞线程
monitor -c 5 *Test* printParams # 方法监控
Async-profiler:低开销性能分析
bash复制./profiler.sh -d 30 -f flamegraph.html PID
Prometheus + Grafana:监控可视化
JVM参数模板:
code复制-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
不要盲目重启:
谨慎使用jmap:
线程池陷阱:
java复制// 错误示例:无界队列可能导致OOM
Executors.newFixedThreadPool(200);
// 正确做法:使用有界队列
new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000));
缓存使用规范:
在实际工作中,我总结出一个有效的排查流程:先看监控(确定问题范围)→ 保留现场(收集诊断数据)→ 分析原因(使用专业工具)→ 验证修复(灰度发布)。这个过程可能需要多次迭代,重要的是保持冷静,用数据说话而不是凭猜测行事。