1. 概述
本文将介绍一些 IntelliJ 中高级调试功能,这些技巧在排查复杂问题时非常实用。
我们假设你已经掌握了调试的基础操作(如启动调试、Step Into、Step Over 等)。如果你还不熟悉这些基本操作,建议先参考 这篇文章 进行学习。
2. 智能步入(Smart Step Into)
在一行代码中调用多个方法的情况很常见,例如:
doJob(getArg1(), getArg2());
如果你直接按 F7
(Step Into),调试器会按照 JVM 的求值顺序依次进入方法:先 getArg1()
,再 getArg2()
,最后才进入 doJob()
。
但很多时候我们只想直接进入目标方法,跳过中间的参数调用。这时候就可以使用 Smart Step Into 功能。
✅ **默认快捷键是 Shift + F7
**,调出后你可以选择要进入的方法:
注意:IntelliJ 会把最外层的方法放在列表顶部,所以你可以直接按 Shift + F7
然后回车,快速进入目标方法。
3. 丢弃栈帧(Drop Frame)
有时候我们意识到某个关键处理已经执行过了(比如方法参数的计算),但我们还想重新执行它。
这时候可以使用 Drop Frame 功能,即丢弃当前 JVM 栈帧,让程序“回退”到上一步。
举个例子:
假设我们想重新调试 getArg1()
的处理逻辑,可以先丢弃当前的 doJob
栈帧:
此时我们会回到上一层方法:
但由于参数已经计算过了,我们还需要继续丢弃当前栈帧:
✅ 现在我们可以重新执行处理逻辑了,只需要按 F7
(Step Into)即可。
4. 字段断点(Field Breakpoints)
有时候第三方库中的字段是被直接修改的(而不是通过 setter),我们很难追踪这些字段是在哪里被修改的。
IntelliJ 提供了 字段断点(Field Breakpoint),可以帮助你监控字段的读写操作。
设置方式和普通断点一样:在字段行左侧点击即可。然后右键断点图标,打开属性面板,选择你关注的是字段的 读取、写入,还是两者都监控:
5. 日志断点(Logging Breakpoints)
在排查并发问题(如竞态条件)时,我们可能不想中断程序执行,只想记录某些方法的调用信息。
IntelliJ 提供了 日志断点(Logging Breakpoint):命中时不会暂停程序,而是输出日志。
来看一个例子:
public static void main(String[] args) {
ThreadLocalRandom random = ThreadLocalRandom.current();
int count = 0;
for (int i = 0; i < 5; i++) {
if (isInterested(random.nextInt(10))) {
count++;
}
}
System.out.printf("Found %d interested values%n", count);
}
private static boolean isInterested(int i) {
return i % 2 == 0;
}
假设我们想记录 isInterested
方法每次被调用时传入的参数。
✅ 设置方式:按住 Shift
点击左侧行号区域,创建一个非阻塞断点。然后右键断点,设置要记录的表达式:
运行程序(必须使用 Debug 模式),你会看到类似输出:
isInterested(1)
isInterested(4)
isInterested(3)
isInterested(1)
isInterested(6)
Found 2 interested values
6. 条件断点(Conditional Breakpoints)
如果一个方法被多个线程并发调用,而你只想在特定条件下中断程序,可以使用 条件断点。
✅ 在断点处右键,设置一个布尔表达式,只有满足条件时才会触发断点。
例如:
此时,只有当传入参数大于 3 时,调试器才会中断。
7. 对象标记(Object Marks)
这是 IntelliJ 最强大但也最不为人知的功能之一。简单来说,你可以给 JVM 中的对象打上自定义标签,方便在调试时快速识别和追踪。
我们用以下代码演示:
public class Test {
public static void main(String[] args) {
Collection<Task> tasks = Arrays.asList(new Task(), new Task());
tasks.forEach(task -> new Thread(task).start());
}
private static void mayBeAdd(Collection<Integer> holder) {
int i = ThreadLocalRandom.current().nextInt(10);
if (i % 3 == 0) {
holder.add(i);
}
}
private static class Task implements Runnable {
private final Collection<Integer> holder = new ArrayList<>();
@Override
public void run() {
for (int i = 0; i < 20; i++) {
mayBeAdd(holder);
}
}
}
}
7.1 创建标记
✅ 当程序在断点处暂停,并且目标对象在当前栈帧中可达时,你可以选中对象并按 F11
(Mark Object),然后给它起个名字:
7.2 查看标记
✅ 之后你可以在调试器的任何地方看到这个自定义标签:
即使对象当前不在栈帧中,你依然可以查看它的状态。只需打开 Evaluate Expression 面板,输入标签名,IntelliJ 会自动提示你加上 _DebugLabel
后缀:
✅ 执行后即可看到对象的内部状态:
7.3 用标记作为断点条件
✅ 你还可以在断点条件中使用这些标签:
8. 总结
本文介绍了多个在调试多线程程序时非常实用的技巧,包括:
- Smart Step Into:跳过中间调用,直达目标方法
- Drop Frame:回退执行,重新调试
- Field Breakpoints:监控字段读写
- Logging Breakpoints:无中断日志记录
- Conditional Breakpoints:按条件中断
- Object Marks:对象打标签,跨方法追踪
这些功能虽然不是每天都会用到,但在处理复杂调试场景时,可以极大提升效率。调试工具用得好,事半功倍。