2862 字
约 9 分钟
6
S01-J04 · IDEA 调试与源码阅读:断点、求值、远程调试

适用读者:只会打行断点、靠 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. 自测

  1. 日志断点与 System.out.println 的本质区别是什么?为什么前者更安全?
  2. 异常断点"在抛出点停"与"在 catch 停",栈信息差在哪?
  3. Evaluate 里调用 list.clear() 会发生什么?
  4. suspend=y 与 suspend=n 分别适合什么场景?
  5. 为什么 JDWP 端口绝不能暴露到公网?正确的远程连法是什么?

15. 下一篇

S01-J05 · IDE 性能与大工程:内存、索引、插件成本

调试能力上去之后,下一步的瓶颈往往是 IDE 本身:项目一大就卡、索引一跑就是几分钟。下一篇用"三档堆内存 × 插件开关"的对照方法,把"卡"拆成可测量的指标,并讲清索引重建的代价与远程开发的取舍。


本篇属于系列《S01 · 开发环境与工具链》。旧文来源:A-1852;扩展点 E-S01-019/020。JDWP 启动输出为 2026-11-05 实测。

S01-J04 · IDEA 调试与源码阅读:断点、求值、远程调试
https://www.clxhxhhr.top/posts/5169/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。