Java根因定位案例

wen java案例 2

本文目录导读:

Java根因定位案例

  1. 案例一:CPU飙高问题排查
  2. 案例二:内存溢出(OOM)排查
  3. 案例三:死锁问题排查
  4. 案例四:线上Full GC频繁
  5. 案例五:接口响应慢
  6. 排查工具速查表

我来分享几个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 1000
Arthas 在线诊断 各种实时诊断命令
MAT 堆分析 分析OOM根因
GCeasy GC日志分析 分析GC问题

根因定位的通用思路:

  1. 监控告警:第一时间发现问题
  2. 现场保留:保存日志、dump文件
  3. 问题复现:尝试复现问题
  4. 工具分析:使用合适工具定位
  5. 代码审查:定位到具体代码行
  6. 修复验证:修复后验证效果

不要凭经验猜测,要基于数据定位,每个问题都有痕迹,关键是找到正确的排查路径。

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