共计 4027 个字符,预计需要花费 11 分钟才能阅读完成。
Java 中各种 IDE 的 Debug 功能,都是通过 Java 提供的 Java Platform Debugger Architecture (JPDA) 来实现的。
借助 Debug 功能,可以很方便的调试程序,快速的模拟 / 找到程序中的错误。
Interllij Idea 的 Debug 功能上说虽然看起来和 Eclipse 差不多,但是在使用体验上,还是要比 Eclipse 好了不少。
Debug 中,最常用的莫过于下一步,下一个断点(Breakpoint),查看运行中的值几个操作;但是除了这些 IDE 还提供了一些“高级”的功能,可以帮助我们更方便的进行调试
Java8 Streams Debug
Stream 作为 Java 8 的一大亮点,它和 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。
IntStream.iterate(1, n -> n + 1)
.skip(100)
.limit(100)
.filter(PrimeFinder::isPrime)// 检查是否是素数
.forEach(System.out::println);
上面这段代码,就是一个 streams 的常见用法,对集合排序并转换取值。Idea 也提供了分析 streams 过程的功能
修改程序执行流程
在 Debug 的过程中,一般情况下,让程序正常执行即可。但是某些情况下,需要动态的修改执行流程,此时如果通过修改代码的方式还是太不方便了,好在 Idea 提供了一些动态修改程序执行流程的功能,可以让我们很灵活的进行调试
返回上一个栈帧 / 删除当前栈帧 /“逆向运行”(Drop frame)
当我们在 Debug 时出现手抖等情况,提前或按错了下一步,导致错过了断点。此时可以通过 Idea 提供的 Drop Frame 功能,来返回到上一个栈帧
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)[插图] 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
其实不光是 Java,其他编程语言的方法执行模型,也是一个栈结构,方法的执行对应着一次 push/pop 的操作
比如下面这段代码,当执行过一次方法后,栈帧上有两个方法
此时,点击 Drop Frame 按钮后,会删除栈顶上的数据,回到调用 log 方法前的位置
注意:Drop Frame 虽然好用,但是可能在 Drop Frame 之后发生一些不可逆的问题,比如 IO 类的操作,或已修改的共享变量是无法回滚的,因为这个操作只是删除栈顶的栈帧,并不是真正的“逆向运行”
强制方法返回(Force Return)
当一个方法比较长,或者 Step Info 到一个不太重要的方法想跳过该方法时,可以通过 Force Return 功能来强制结束该方法
注意:Force Return 和 Step Out 不一样,Step Out 是跳出当前步骤,还是会执行方法中的代码;而 Force Return 是直接强制结束方法,跳过该方法后的所有代码直接返回。比如下面这段代码,当使用 Force Return 后,evaluate 方法中的 println 并不会执行
当要强制返回的方法有返回值时(非 void),force return 还需要指定一个返回值
触发异常
当调用的方法可能抛出异常,调用者需要处理异常时,可以直接让方法抛出异常而不用修改代码
下面是一段伪代码,模拟发送请求,超时自动重试
当方法执行至 sendPacket 时,可以执行 Throw Exception 操作,提前结束方法并抛出指定的异常
调用者收到异常后,就可以执行 catch 中的重试逻辑了,这样以来就不用通过修改程序等操作来模拟异常,非常的方便
Debug 运行中的 JVM 进程(Attach to Process)
当应用程序无法在 Idea 中运行,又想 Debug 这个运行中的程序时,可以通过 Attach to Process 功能,该功能可以 Debug 做到调试运行中的程序,当然前提是,保证这个正在运行的 JVM 进程代码和 Idea 中的代码一致
这种场景其实挺常见的,比如你要调试 springboot executable jar 时,或者调试 tomcat 源码等独立部署运行的进程,通过 Attach to Process 就非常方便了,可以做到用 Idea 之外的环境 +Idea 中的代码进行 Debug
这种功能其实在 C /C++ GDB 下也有,Debug 正在运行的程序而已,Intellij Clion 也是支持的
远程调试(Remote Debug)
远程调试是 JVM 提供的功能,和上面的 Attach to Process 类似,只是这个进程从本地变成远程了
比如我们的程序在本地没有问题,在服务器上却有问题;比如本地是 MacOs,服务器是 Centos,环境的不同导致出现某些 Bug,此时就可以通过远程调试功能来调试
如果要启用远程调试,需要在远程 JVM 进程的启动脚本中添加以下参数:
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
suspend 参数表示,JVM 进程是否已“挂起”模式启动,如果以“挂起”模式启动,JVM 进程会一直阻塞不继续执行,直到远程调试器连接到该进程为止
这个参数非常有用,比如我们的问题是在 JVM 启动期间发生的(比如 Spring 的加载 / 初始化流程),就需要将 suspend 设置为 y,这样 JVM 进程就会等待 Ide 中的远程调试连接完成才会继续运行。否则远程的 JVM 已经运行了一段时间了,Ide 的 Debugger 才连接,早已经错过了断点的时机
在远程 JVM 进程配置完成 Debug 模式并启动完成后,就可以在 Idea 中连接了,在 Idea 的 Run/Debug Configurations 面板中新建一个 Remote 的 Configuration:
然后配置好 Host/Port,点击 Apply 保存即可
最后,先启动远程的 JVM 进程,然后在 Idea 中已 Debug 来运行刚才配置的 Configuration 即可
小提示:远程调试下,由于有网络的开销,反应会比较慢,而且会导致远程程序的暂停,使用时请找一个没有人使用的环境
多线程下的调试
多线程程序是比较难写的,确切的说是很难调试,一个不小心就会因为线程安全的问题引起各种 Bug,并且这些 Bug 还可能很难复现。由于操作系统的线程调度是我们无法控制的,所以多线程程序的错误有很大的随机性,一旦出现问题很难找到;我们的程序可能在 99.99% 的情况下都是正常的,但是最后的 0.01% 也很可能造成严重的错误
线程安全的最常见问题就是竞争条件,当某些数据被多个线程同时修改时,就可能会发生线程安全问题
比如下面这个流程,正常情况下程序没问题
当出现了竞争问题,单个线程的 read 和 write 操作之间,调度了其他线程,此时数据就会出错
下面是一段示例代码,虽然共享数据 a 是一个 synchronizedList,但是它并不能保证 addIfAbsent 是个原子操作,因为 contains 和 add 是两个 synchronized 方法,两个方法的执行间隙间还是有可能被其他线程修改
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ConcurrencyTest {static final List a = Collections.synchronizedList(new ArrayList());
public static void main(String[] args) {Thread t = new Thread(() -> addIfAbsent(17));
t.start();
addIfAbsent(17);
t.join();
System.out.println(a);
}
private static void addIfAbsent(int x) {if (!a.contains(x)) {a.add(x);
}
}
}
如果对这段代码进行 Debug 时,一个 Step Over(下一步)之后,这个下一步操作的作用域是整个进程,而不是当前进程。也就是说,Debug 下一步之后,很可能被其他线程插入并执行了修改,这个共享数据 a 一样不安全,很可能出现重复添加元素 17 的问题
但是上述问题只是可能出现,实际调试时很难复现。Idea 的 Debug 可以将挂起粒度设置为线程,而不是整个引用
Suspend 设置为 Thread 后,如下图所示,将断点打在 a.add 这一行,然后以 Debug 模式运行程序后,主线程和新建的线程都会挂在 addIfAbsent 方法中,我们可以在 Idea 中的 Debug 面板中切换线程
此时,Main 线程和子线程都已经调用了 contains 方法,并都返回 false,挂起在 a.add 这一行,都准备将 17 添加到 a 中
执行下一步后,Main 线程成功的将 17 添加到集合中
此时切换到 Thread- 0 线程,还是挂在 a.add(x) 这一行,但是集合 a 中已经有元素 17 了,但时 Thread- 0 线程还是会继续 add,add 之后集合 a 就出现了重复元素 17,导致程序出现了 bug
从上面的例子可以看出,在调试多线程程序的过程中,利用 Idea Debug 的 Suspend 功能,可以很方便的模拟多线程竞争的问题,这对于编写或调试多线程程序实在太方便了
参考
- Java Platform Debugger Architecture (JPDA) | Oracle Docs
- Debugging Applications | Oracle Docs
- Debug code – Help | IntelliJ IDEA