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**,调出后你可以选择要进入的方法:

trick1

注意:IntelliJ 会把最外层的方法放在列表顶部,所以你可以直接按 Shift + F7 然后回车,快速进入目标方法。

3. 丢弃栈帧(Drop Frame)

有时候我们意识到某个关键处理已经执行过了(比如方法参数的计算),但我们还想重新执行它。

这时候可以使用 Drop Frame 功能,即丢弃当前 JVM 栈帧,让程序“回退”到上一步。

举个例子:

trick2

假设我们想重新调试 getArg1() 的处理逻辑,可以先丢弃当前的 doJob 栈帧:

trick3

此时我们会回到上一层方法:

trick4

但由于参数已经计算过了,我们还需要继续丢弃当前栈帧:

trick5

✅ 现在我们可以重新执行处理逻辑了,只需要按 F7(Step Into)即可。

4. 字段断点(Field Breakpoints)

有时候第三方库中的字段是被直接修改的(而不是通过 setter),我们很难追踪这些字段是在哪里被修改的。

IntelliJ 提供了 字段断点(Field Breakpoint),可以帮助你监控字段的读写操作。

设置方式和普通断点一样:在字段行左侧点击即可。然后右键断点图标,打开属性面板,选择你关注的是字段的 读取、写入,还是两者都监控

trick6

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 点击左侧行号区域,创建一个非阻塞断点。然后右键断点,设置要记录的表达式:

trick7

运行程序(必须使用 Debug 模式),你会看到类似输出:

isInterested(1)
isInterested(4)
isInterested(3)
isInterested(1)
isInterested(6)
Found 2 interested values

6. 条件断点(Conditional Breakpoints)

如果一个方法被多个线程并发调用,而你只想在特定条件下中断程序,可以使用 条件断点

✅ 在断点处右键,设置一个布尔表达式,只有满足条件时才会触发断点。

例如:

trcik8

此时,只有当传入参数大于 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),然后给它起个名字:

trick9

7.2 查看标记

✅ 之后你可以在调试器的任何地方看到这个自定义标签:

trick10

即使对象当前不在栈帧中,你依然可以查看它的状态。只需打开 Evaluate Expression 面板,输入标签名,IntelliJ 会自动提示你加上 _DebugLabel 后缀:

trick11

✅ 执行后即可看到对象的内部状态:

trick12

7.3 用标记作为断点条件

✅ 你还可以在断点条件中使用这些标签:

trick13

8. 总结

本文介绍了多个在调试多线程程序时非常实用的技巧,包括:

  • Smart Step Into:跳过中间调用,直达目标方法
  • Drop Frame:回退执行,重新调试
  • Field Breakpoints:监控字段读写
  • Logging Breakpoints:无中断日志记录
  • Conditional Breakpoints:按条件中断
  • Object Marks:对象打标签,跨方法追踪

这些功能虽然不是每天都会用到,但在处理复杂调试场景时,可以极大提升效率。调试工具用得好,事半功倍。


原始标题:IntelliJ Debugging Tricks

« 上一篇: Java 包机制详解
» 下一篇: Java Weekly, 第260期