本文目录导读:

- 建立可复现的步骤
- 从日志和异常入手
- 断点调试(IDE调试工具)
- 日志分析与追踪
- 代码审查与静态分析
- Profiling 与性能分析
- 常见类型Bug的针对性排查
- 隔离与二分法
- 善用版本控制与差异比较
- 思维框架:假设驱动排错
- 小结
排查Java代码中的Bug是一个系统化的过程,通常涉及从现象到根源的逆向推理,下面是一套通用的排查思路和常用工具方法,适用于大多数Java项目。
建立可复现的步骤
在开始排查之前,必须确保能够稳定复现Bug,以减少变量干扰,如果无法直接复现,可以通过以下方式辅助:
- 使用日志:增加日志输出(
System.out.println或 Logger.log),记录关键变量的值、执行路径和方法返回结果。 - 编写测试用例:用 JUnit 编写最小化的复现测试,验证特定输入下的行为是否符合预期。
从日志和异常入手
- 查看完整异常栈:重点关注栈顶信息(异常发生的具体方法和行号)以及根因(
Caused by部分)。 - 添加/提升日志级别:若现有日志信息不足,可以将相关类的日志级别临时改为
DEBUG或TRACE,观察更细粒度的输出。 - 使用
-verbose:class:检查是否加载了不正确的类或版本冲突。
断点调试(IDE调试工具)
对于复杂的逻辑或并发问题,断点调试是最直观的方法:
- 设置断点:在可能出错的代码行设置断点(包括方法入口、条件、循环内部等)。
- 条件断点:右击断点图标,输入触发条件(如
i == 100),避免大量重复中断。 - Step Over / Step Into / Frame:逐行执行,观察变量值的变化;Step Into 会进入方法内部,而 Drop Frame 可以回溯到上一帧重新执行。
- 表达式求值(Evaluate Expression):在断点处右键输入表达式,动态计算当前上下文的值。
注意:调试并发问题时,断点可能干扰线程调度,此时可以考虑日志或线程转储(Thread Dump)。
日志分析与追踪
- 在关键方法入口和出口添加
System.currentTimeMillis():记录耗时,定位性能瓶颈。 - 使用AOP统一记录输入参数与返回值:减少手动埋点的工作量。
- 对于分布式系统:引入Trace ID或Request ID,通过日志关联不同模块的操作。
代码审查与静态分析
- 肉眼检查:重点关注常见的错误模式:
- 空指针(没有判空)
- 集合修改异常(ConcurrentModificationException)
- 资源未关闭(IO、数据库连接、Socket)
- 线程安全问题(未同步的共享变量、死锁)
- 对象比较( 误用 vs
equals()) - 异常处理不当(
catch后无返回值或吞掉异常)
- 工具检查:
- FindBugs / SpotBugs:扫描常见的Bug模式(如
NullPointerException风险、死循环、资源泄漏等)。 - Checkstyle / PMD:检查代码风格与潜在错误(未使用的变量、空catch块等)。
- IntelliJ IDEA 的 Inspect Code:一键扫描整个项目,结果包含错误、警告、性能问题。
- FindBugs / SpotBugs:扫描常见的Bug模式(如
Profiling 与性能分析
当问题涉及内存泄漏、CPU占用高、线程死锁时,需要更专业的工具:
- VisualVM(JDK自带):监控堆内存、线程状态、GC活动;可以生成堆转储(Heap Dump)分析对象引用链。
- JProfiler / YourKit:更强大的商业工具,支持方法级CPU采样、内存视图、数据库连接池分析。
- JMC(JDK Mission Control):配合 Flight Recorder 做无侵入的运行时数据采集。
常见类型Bug的针对性排查
| Bug类型 | 排查方法 |
|---|---|
| 空指针异常 | 在可能为null的对象上加入 Objects.requireNonNull;使用 Optional 避免深层判空;查看栈顶行号,检查该行所有 之前的变量。 |
| 线程安全问题 | 启用 Thread Dump(jstack <pid>)查看是否有锁等待;使用 -XX:+ThreadDumpOnCrash;检查共享变量是否有 volatile 或 synchronized。 |
| 内存泄漏 | 使用 VisualVM 观察堆使用曲线是否持续上升;生成多个 Heap Dump 对比,查找不断增长的对象类型。 |
| 数据库连接池耗尽 | 启用连接池的监控(如 HikariCP 的 pool.stats);检查是否在 finally 块中关闭了连接。 |
| 并发修改异常 | 在 foreach 循环中对集合进行结构修改(增删元素);应使用迭代器的 remove 方法或 ConcurrentHashMap。 |
隔离与二分法
如果一个Bug难以定位,可以使用“二分法”逐步缩小范围:
- 注释掉一半的代码(或使用版本控制回退到早期提交)。
- 观察Bug是否消失。
- 根据结果缩小到某一半代码中。
- 继续二分,直至找到最小可复现片段。
例如:系统运行一段时间后出现OOM,可以逐步删除业务模块或调整参数,观察内存泄漏是否消失。
善用版本控制与差异比较
- Git Bisect:当Bug在新版本中出现但不知道该从哪个提交开始时,使用
git bisect标记好坏的提交,自动进行二分搜索找到引入Bug的提交。 - 代码审查:对比两个版本的差异,重点关注修改处。
- 回退验证:将某些改动暂时回退,确认该改动是否是Bug的源头。
思维框架:假设驱动排错
不要盲目尝试,先提出合理的假设,再设计验证步骤,
- 假设:用户名输入为空导致 NullPointerException。
- 验证:在代码中找到接收该输入的方法,检查是否对空值做了处理。
- 测试:输入空字符串,观察是否抛出异常。
如果假设错误,换下一个可能性较接近的假设。
小结
排查Java Bug时,遵循由外到内、由易到难的原则:
- 先看日志和异常栈 →
- 再使用IDE断点调试 →
- 必要时借助Profiling工具分析内存或线程 →
- 最后结合代码审查与假设验证。
切忌:一出现Bug就只往深层代码钻或直接尝试各种配置改动,容易忽略最简单的拼写错误或配置问题,先从最浅层、最频繁出现的模式开始排查,通常是最有效的路径。