
Java中各种IDE的Debug功能都是通过Java提供的Java Platform Debugger Architecture(JPDA)来实现的。
借助调试功能,您可以轻松调试程序并快速模拟/查找程序中的错误。
虽然 Interllij Idea 的 Debug 功能看起来和 Eclipse 差不多,但在体验上还是比 Eclipse 好很多。
在Debug中,最常用的是下一步,下一个断点(Breakpoint),以及查看运行值的几个操作;但是除了这些IDE之外,它还提供了一些“高级”的功能,可以帮助我们更方便的调试。
Java8 流调试
Stream 作为 Java 8 的一大亮点,与 java.io 包中的 InputStream 和 OutputStream 是完全不同的概念。Java 8中的Stream是对集合(Collection)对象功能的增强,专注于对集合对象进行各种非常方便高效的聚合操作(aggregate operations),或者批量数据操作(bulk data operations)。
IntStream.iterate(1, n -> n + 1) .skip(100) .limit(100) .filter(PrimeFinder::isPrime)//检查是否是素数 .forEach(System.out::println);复制代码
上面的代码是流、排序集合和转换值的常见用法。Idea还提供了分析流过程的能力
修改程序执行流程
在调试过程中,一般情况下程序是可以正常执行的。但是,在某些情况下,需要动态修改执行过程。这个时候修改代码太不方便了。好在Idea提供了一些函数来动态修改程序的执行过程,让我们可以灵活调试。
返回上一个堆栈帧/删除当前堆栈帧/“丢帧”
当我们在Debugging时遇到手抖,提前或按错下一步,导致漏断断点。此时可以通过Idea提供的Drop Frame函数返回到之前的栈帧
虚拟机栈描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)【图解】来存储局部变量表、操作数栈、动态链接、方法出口等信息, 等等。 。每个方法从调用到执行完成的过程,对应一个栈帧被压入虚拟机栈中的栈的过程。
其实不光是Java,其他编程语言的方法执行模型也是栈结构。方法的执行对应于 push/pop 操作。
比如下面的代码,当方法执行一次时,栈帧上有两个方法
此时点击Drop Frame按钮后,栈顶的数据会被删除,回到调用log方法之前的位置。
注意:虽然 Drop Frame 好用,但是 Drop Frame 之后可能会出现一些不可逆的问题伪代码用什么软件写,比如 IO 类操作,或者修改后的共享变量不能回滚,因为这个操作只会删除栈顶的栈帧伪代码用什么软件写,而不是真的“倒退”
强制返回
当一个方法比较长,或者 Step Info 到达一个不太重要的方法,想跳过该方法时,可以使用 Force Return 函数来强制结束该方法
注意:Force Return 与 Step Out 不同。Step Out 跳出当前步骤或执行方法中的代码;而Force Return则直接强制方法结束,跳过方法后面的所有代码,直接返回。比如下面的代码,当使用Force Return时,evaluate方法中的println不会被执行
当要强制返回的方法有返回值(非void)时,强制返回也需要指定返回值
触发异常
当被调用的方法可能抛出异常,调用者需要处理异常时,该方法可以直接抛出异常,无需修改代码
下面是模拟发送请求并随着时间的推移自动重试的伪代码
方法执行到sendPacket时,可以进行Throw Exception操作,提前结束方法,抛出指定的异常
调用者收到异常后,可以执行catch中的重试逻辑,这样就不用修改程序来模拟异常,非常方便
调试正在运行的 JVM 进程(附加到进程)
当应用程序无法在 Idea 中运行,而您想调试正在运行的程序时,可以使用 Attach to Process 功能,该功能可以调试正在运行的程序。当然前提是要保证运行的JVM进程代码和Idea中的代码一致
这种情况其实很常见。比如想调试springboot可执行jar,或者调试tomcat源码等独立部署运行的进程,使用Attach to Process就非常方便。您可以使用 Idea + Idea 以外的环境。调试代码
其实这个功能在C/C++ GDB下也是可以的。它只是Debug的运行程序,Intellij Clion也支持。
远程调试
远程调试是JVM提供的一个功能,和上面的Attach to Process类似,只是进程从本地变成了远程
比如我们的程序本地没有问题,但是服务器有问题;比如本地是MacOs,服务器是Centos,由于环境不同会出现一些bug。这时候我们就可以通过远程调试功能进行调试了。
如果要开启远程调试,需要在远程JVM进程的启动脚本中添加如下参数:
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005复制代码
挂起参数表示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
温馨提示:远程调试下,由于网络开销,响应会变慢,会导致远程程序挂起。请找一个没有人使用的环境。
多线程下调试
多线程程序很难编写,准确地说,它们也很难调试。由于线程安全问题,一个粗心的进程会导致各种错误,并且这些错误可能难以重现。由于操作系统的线程调度是我们无法控制的,所以多线程程序的错误非常随机,一旦出现问题就很难发现;我们的程序可能在 99.99% 的情况下 OK,但是最后的 0.01% 也很有可能导致严重的错误
线程安全最常见的问题是竞争条件,当多个线程同时修改某些数据时可能会出现这种情况
比如下面的流程,一般情况下,程序是没有问题的
当出现竞争问题时,在单个线程的读写操作之间调度其他线程,数据就会出错。
以下是示例代码。共享数据a虽然是一个synchronizedList,但并不能保证addIfAbsent是原子操作,因为contains和add是两个同步方法,两个方法之间的执行间隙仍有可能被其他线程修改。
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); } }}复制代码
如果调试这段代码,在 Step Over(下一步)之后,下一步的范围是整个进程,而不是当前线程。也就是说,经过下一步Debug之后,很可能会被其他线程插入和修改。这个共享数据a也是不安全的,很可能会出现重复添加元素17的问题。
但是,上述问题只是可能的,在实际调试中很难重现。Idea的Debug可以将暂停粒度设置为线程,而不是整个进程
Suspend设置为Thread后,如下图,在a.add这一行打断点,然后在Debug模式下运行程序,主线程和新创建的线程都会在addIfAbsent方法中挂起,我们可以在面板中的 Idea Switch 线程中调试
此时主线程和子线程都调用了contains方法,都返回false,挂在a.add这一行,准备给a加17。
执行下一步后,主线程成功将17添加到集合中
这时候切换到Thread-0线程的时候,依然挂在a.add(x)这一行,但是set a中已经有元素17了,但是Thread-0线程会继续添加,set a就会添加后重复。元素 17,导致程序中的错误
从上面的例子可以看出,在调试多线程程序的过程中,可以使用Idea Debug的Suspend功能来模拟多线程竞争的问题,非常方便编写或调试多线程程序.
请登录后发表评论
注册
社交帐号登录