适用读者:只会打行断点、靠
System.out.println排查的人;需要读框架源码的人。
前置:S01-B05(快捷键)、S01-J01(可选)。
实测环境:JDK 21 / Ubuntu 24.04.1(JDWP 部分为 2026-11-05 实测);IDEA 2025.x 界面口径。
覆盖旧文:A-1852(4 个阅读源码必备的调试技巧,52 点)。
新增扩展:E-S01-019(JDWP 远程/容器调试)、E-S01-020(条件断点、日志断点、Stream Debugger)。
上一篇:S01-J03 · Git 手术与效率:reset、revert、reflog、bisect、alias
下一篇:S01-J05 · IDE 性能与大工程。
1. 一句话结论
调试的本质是"在不改代码的前提下观察程序"。 行断点只是最粗的一种观察方式;能替代 println 的日志断点、能命中特定数据流的条件断点、能追踪状态的字段断点,才是真正的效率来源。
旧文 A-1852 给的四招(断点处加 log、字段断点、异常断点、方法断点)方向完全正确,本篇补上条件断点、求值、Stream Debugger、远程调试,并把它们组织成"按问题类型选工具"的矩阵。
2. 按问题类型选断点
| 你想知道 | 用哪种断点 | 关键设置 |
|---|---|---|
| 这行代码执行到了吗? | 行断点 | 默认 |
只有 i == 7 时才停 |
条件断点 | Condition: i == 7 |
| 每次调用时的参数值(不想停) | 日志断点(Evaluate and log) | 表达式 + 不挂起线程 |
| 这个字段什么时候被改的? | 字段断点(Watch) | 在字段声明处 Shift+点击,选 Field Watchpoint |
| 抛异常那一刻的栈 | 异常断点 | Any Exception + 类过滤 |
| 这个接口到底进了哪个实现? | 方法断点 | 打在接口方法上,选 Entry |
| 死循环/卡死在哪 | 线程转储 | jstack 或 Debug 面板的 Threads |
3. 日志断点:替代 println(A-1852 第一招)
问题:println 打日志 → 忘记删 → 提交 → CR 被要求删 → 重新提交。一次排查污染了历史。
做法:在行断点上 Shift + 鼠标左键(或右键 → 断点属性),勾 Evaluate and log,填表达式:
"interested " + i
然后以 Debug 模式运行。正常模式运行不会打印——所以这段"日志"永远不会污染代码。
旧文给的真实输出:
interested 7
interested 5
interested 1
interested 2
interested 0
Found 2 interested values
再勾 Breakpoint hit message,每行前面会附带命中的位置:
Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 6
关键细节:记得取消 Suspend(把断点变成"仅打日志不暂停"),否则每次都停,跟普通断点没区别。
| 用途 | 表达式示例 |
|---|---|
| 看参数 | "id=" + id + ", type=" + type |
| 看返回值(方法出口断点) | "ret=" + this.someValue |
| 看集合大小 | "size=" + list.size() |
| 条件打印 | i > 5 ? "big " + i : "" |
4. 条件断点:只在特定数据上停
场景:循环 10 万次,只有某一条数据有问题;或者并发下只有某个 userId 出错。
Condition: userId == 42
或
Condition: order.getAmount() > 10000
代价:条件表达式会在每次命中行时求值。写在热路径里(如百万次循环的方法体)会显著变慢——这时改用日志断点 + 后置过滤,或把条件写在方法入口。
高级技巧:条件里可以调用方法(
list.contains(x)),但别调用有副作用的(改状态、发请求),否则你在调试时改变了程序行为。
5. 字段断点:抓"谁改了我的值"(A-1852 第二招)
在字段声明处 Shift+点击 → 选 Field Watchpoint,可选:
| 选项 | 含义 |
|---|---|
| Field access | 读也停(噪音大) |
| Field modification | 只在写时停(推荐) |
典型用途:一个 status 莫名其妙变成 null,你不知道是哪个分支改的。打上字段断点,它会停在那次赋值的调用栈上——直接看到"谁改的、从哪来"。
6. 异常断点:在"抛出点"停,而不是"捕获点"(A-1852 第三招)
默认 Any Exception 会在所有异常上停——包括框架内部大量被吞掉的受检异常,噪音极大。正确配置:
Debug 面板 → View Breakpoints → + → Java Exception Breakpoints
Exception class: java.lang.NullPointerException ← 只停你要的
☐ Caught exceptions(默认不勾)
☑ Uncaught exceptions
☑ On throw(关键:在抛出点停,而不是 catch 点)
"在抛出点停"与"在 catch 停"的差别:栈信息量完全不同。在 catch 点停,你看到的是"处理异常的地方";在抛出点停,你看到的是"异常产生的地方"——后者才是根因所在。
7. 方法断点:接口实现到底走了哪个(A-1852 第四招)
把断点打在接口方法上(不是实现类),IDEA 会在所有实现的入口停。这在读框架源码时极其有用:
- 你只有一个
UserService接口,实现有 3 个(含代理、Mock、AOP 代理)→ 方法断点告诉你实际走的是哪个。 - 但方法断点的性能开销最大(JVM 需要插桩),别在热路径方法上长期保留。
8. 求值与快速查看
| 动作 | 快捷键 | 用途 |
|---|---|---|
| Evaluate Expression | Alt+F8 |
在暂停状态下执行任意表达式 |
| Quick Evaluate | Ctrl+Alt+F8(选中表达式) |
不弹窗直接看结果 |
| Quick Definition | Ctrl+Shift+I |
不跳走,看方法/类定义 |
| 添加到 Watches | 右键 → Add to Watches | 持续观察某表达式 |
Evaluate 的常见误用:在 Evaluate 里改了对象状态(list.clear()),以为只是"看看"。它真的会执行——调试时改状态是排查"行为不一致"类问题的头号元凶。
9. Stream Debugger:看链式调用每一步
Java 8 之后大量代码是 stream().filter().map().collect(),行断点只能看整体。IDEA 提供 Stream Debugger:断点停在 stream 上时,点 Debug 面板的 Trace Current Stream Chain,能看到每一步的输入输出。
| 用途 | 说明 |
|---|---|
| 定位"在哪一步元素丢了" | 逐步看 size |
确认 filter 条件是否符合预期 |
看每步元素 |
前提:断点必须停在该 stream 的求值过程中(链式调用上),否则按钮是灰的。
10. 远程调试:JDWP(实测)
10.1 原理与开关
JVM 内置调试协议 JDWP。启动时加参数即可:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:5005 -jar app.jar
实测输出(JDK 21 / Ubuntu 24.04.1):
Listening for transport dt_socket at address: 5005
sum=15
第一行证明调试端口已就绪,第二行是程序本身跑完。各参数含义:
| 参数 | 含义 | 建议 |
|---|---|---|
transport=dt_socket |
走 TCP | 固定 |
server=y |
被调试进程作为服务端等连接 | 固定 |
suspend=n |
启动后不等待调试器,直接跑 | 生产/容器用 n;y 会卡在启动等连接 |
address=127.0.0.1:5005 |
监听地址 | 永远绑 127.0.0.1,见 §10.4 |
10.2 IDEA 侧
Run → Edit Configurations → + → Remote JVM Debug
Host: 127.0.0.1 Port: 5005
→ 复制它给的 JVM 参数(就是上面那条 -agentlib)
→ Debug 启动
10.3 容器内调试
docker run -e JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" -p 5005:5005 app
| 坑 | 说明 |
|---|---|
address=*:5005 |
容器内必须绑 *(0.0.0.0),否则容器外连不上 |
| 端口映射 | -p 127.0.0.1:5005:5005(只在宿主机本地暴露) |
JAVA_TOOL_OPTIONS |
免改启动脚本;注意它会打印一行 Picked up JAVA_TOOL_OPTIONS |
10.4 安全边界(必须写清楚)
JDWP 没有认证机制。 任何能连上该端口的人都能:读内存、改变量、执行任意代码。所以:
| 规则 | 原因 |
|---|---|
只绑 127.0.0.1 |
不暴露到公网 |
| 用 SSH 端口转发连远程 | ssh -L 5005:127.0.0.1:5005 user@host |
| 生产环境默认不开 | 需要时临时开、用完立刻关 |
| 不在云安全组放行 5005 | 等于把 JVM 交给互联网 |
11. 读框架源码:三件套
| 需求 | 做法 |
|---|---|
| 看不到源码(只有 class) | Ctrl+点击 进入时若提示"无源码",点 Download Sources;Maven 项目可在 Maven 面板勾 Download Sources |
| 想看第三方库实现 | IDEA 自带反编译(.class 直接可读,只是无注释) |
| 想知道"框架在哪一步调了我的代码" | 方法断点打在框架接口上(如 HandlerInterceptor#preHandle) |
读源码的正确顺序:先跑起来 + 断点看调用栈 → 再看关键方法实现 → 最后看设计文档。从文档开始读源码,效率最低。
12. 断点不生效的五种原因
| 现象 | 原因 | 修法 |
|---|---|---|
| 断点是灰的、带禁止符 | 类未加载 / 代码不是最新的 | Build → Rebuild,或 Invalidate Caches(S01-B09 §4) |
| 停在了别处 | 代码与运行版本不一致(增量编译问题) | Rebuild;确认没有多处同名类 |
| 断点跳过不停 | JIT 优化 / 该行无可执行代码(如纯声明) | 换到有语句的行 |
| 条件断点永假 | 表达式类型不匹配(Long vs long 的 ==) |
用 .equals() 或 Objects.equals |
| 远程调试连不上 | 端口未映射 / suspend=y 卡住 / 绑了 127.0.0.1 却从外部连 |
按 §10.3 检查 |
13. 旧文对照
| 旧文(A-1852) | 内容 | 本篇处理 |
|---|---|---|
| 断点处添加 log | Shift+点击 → Evaluate and log → 输出示例 |
§3 保留,补"取消 Suspend"这个关键操作与表达式示例表 |
| 字段断点 | Watchpoint | §5 保留,补 access/modification 的取舍 |
| 异常断点 | Any Exception |
§6 保留,补"On throw vs 在 catch 停"的区别与噪音控制 |
| 方法断点 | 接口方法入口 | §7 保留,补"性能开销最大、别长期保留" |
| — | 无 | 新增 §4 条件断点、§8 求值与误用、§9 Stream Debugger、§10 JDWP 实测、§11 读源码三件套、§12 断点不生效排查 |
14. 自测
- 日志断点与
System.out.println的本质区别是什么?为什么前者更安全? - 异常断点"在抛出点停"与"在 catch 停",栈信息差在哪?
Evaluate里调用list.clear()会发生什么?suspend=y与suspend=n分别适合什么场景?- 为什么 JDWP 端口绝不能暴露到公网?正确的远程连法是什么?
15. 下一篇
S01-J05 · IDE 性能与大工程:内存、索引、插件成本
调试能力上去之后,下一步的瓶颈往往是 IDE 本身:项目一大就卡、索引一跑就是几分钟。下一篇用"三档堆内存 × 插件开关"的对照方法,把"卡"拆成可测量的指标,并讲清索引重建的代价与远程开发的取舍。
本篇属于系列《S01 · 开发环境与工具链》。旧文来源:A-1852;扩展点 E-S01-019/020。JDWP 启动输出为 2026-11-05 实测。