本文目录导读:

我来分享几个Java根因定位的实战案例,通过具体问题讲解排查思路和方法。
CPU飙高问题排查
问题现象
某服务CPU使用率突然飙升至100%,请求响应变慢。
排查步骤
定位CPU高的线程
# 找到Java进程PID top -Hp <pid> # 查看最消耗CPU的线程
将线程ID转为16进制
printf "%x\n" <thread_id>
使用jstack查看堆栈
jstack <pid> | grep -A 30 <hex_thread_id>
根因定位
发现是某线程在循环调用正则表达式:
// 问题代码
public String sanitize(String input) {
// 灾难:输入可能很长,正则存在灾难性回溯
Pattern pattern = Pattern.compile("(a+)+b");
Matcher matcher = pattern.matcher(input);
return matcher.replaceAll("");
}
解决方案
// 修复:使用非回溯方式或限制输入长度
public String sanitize(String input) {
if (input.length() > 1000) {
input = input.substring(0, 1000);
}
// 改用更高效的正则或直接字符串操作
return input.replaceAll("a+", "");
}
内存溢出(OOM)排查
问题现象
服务频繁OOM,导致重启。
排查步骤
JVM启动参数添加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
分析堆转储文件
# 使用MAT或jvisualvm分析 jvisualvm --openheapdump heapdump.hprof
根因定位
通过分析发现HashMap中存在大量重复对象:
// 问题代码
public class DataCache {
private Map<String, List<Data>> cache = new HashMap<>();
public void processData(List<Data> dataList) {
for (Data data : dataList) {
// 每次调用都创建新key,永远不会命中缓存
String key = data.getId() + "_" + System.currentTimeMillis();
cache.put(key, dataList);
}
}
}
解决方案
public class DataCache {
private Map<String, List<Data>> cache = new ConcurrentHashMap<>();
private Cache<String, List<Data>> guavaCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
public void processData(List<Data> dataList) {
// 使用固定key
String key = buildKey(dataList);
if (!cache.containsKey(key)) {
cache.put(key, dataList);
}
}
}
死锁问题排查
问题现象
某些请求一直无响应,服务不报错也不崩溃。
排查步骤
使用jstack检测死锁
jstack -l <pid> | grep -A 20 "deadlock"
根因定位
发现经典的交叉锁死锁:
// 问题代码
public class AccountService {
public void transfer(Account from, Account to, BigDecimal amount) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
}
// 线程A: transfer(accountA, accountB, 100)
// 线程B: transfer(accountB, accountA, 200)
解决方案
public class AccountService {
public void transfer(Account from, Account to, BigDecimal amount) {
// 按账号ID排序,保证获取锁的顺序一致
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}
}
线上Full GC频繁
问题现象
频繁Full GC,STW时间长,系统吞吐量下降。
排查步骤
启用GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log
使用工具分析GC日志
# 使用GCeasy或在线工具分析
根因定位
通过GC日志发现Metaspace不断增长:
// 问题代码:动态生成大量类
public class DynamicClassFactory {
public Object createClass(String className) {
// 每次调用都动态生成新类,未做缓存
ClassPool pool = new ClassPool(true);
CtClass cc = pool.makeClass(className);
// ...
return cc.toClass();
}
}
解决方案
public class DynamicClassFactory {
private static LoadingCache<String, Class<?>> classCache =
CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, Class<?>>() {
@Override
public Class<?> load(String key) {
return createClassInternal(key);
}
});
public Class<?> getClass(String className) {
return classCache.get(className);
}
}
接口响应慢
问题现象
某个接口平均响应时间从50ms上升到5s。
排查步骤
使用Arthas火焰图
# 安装Arthas curl -O https://alibaba.github.io/arthas/arthas-boot.jar java -jar arthas-boot.jar # 使用profiler生成火焰图 profiler start profiler stop --format html
根因定位
发现某SQL执行超慢,且存在N+1问题:
// 问题代码
public List<Order> getOrders(Long userId) {
List<Order> orders = orderMapper.findByUserId(userId);
for (Order order : orders) {
// N+1查询:每个订单又查一次用户信息
User user = userMapper.findById(order.getUserId());
order.setUserInfo(user.getInfo());
}
return orders;
}
解决方案
public List<Order> getOrders(Long userId) {
// 一次性查询所有订单
List<Order> orders = orderMapper.findByUserId(userId);
// 批量查询用户信息
Set<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
Map<Long, User> userMap = userMapper.findByIds(new ArrayList<>(userIds))
.stream()
.collect(Collectors.toMap(User::getId, u -> u));
// 关联数据
orders.forEach(order ->
order.setUserInfo(userMap.get(order.getUserId()).getInfo()));
return orders;
}
排查工具速查表
| 工具 | 用途 | 常用命令 |
|---|---|---|
| jstack | 查看线程堆栈 | jstack -l |
| jmap | 内存分析 | jmap -heap |
| jstat | GC监控 | jstat -gcutil |
| Arthas | 在线诊断 | 各种实时诊断命令 |
| MAT | 堆分析 | 分析OOM根因 |
| GCeasy | GC日志分析 | 分析GC问题 |
根因定位的通用思路:
- 监控告警:第一时间发现问题
- 现场保留:保存日志、dump文件
- 问题复现:尝试复现问题
- 工具分析:使用合适工具定位
- 代码审查:定位到具体代码行
- 修复验证:修复后验证效果
不要凭经验猜测,要基于数据定位,每个问题都有痕迹,关键是找到正确的排查路径。