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调试多个java服务_编程语言


debug调试多个java服务_java_02


debug调试多个java服务_编程语言_03

修改程序执行流程

在 Debug 的过程中,一般情况下,让程序正常执行即可。但是某些情况下,需要动态的修改执行流程,此时如果通过修改代码的方式还是太不方便了,好在 Idea 提供了一些动态修改程序执行流程的功能,可以让我们很灵活的进行调试

返回上一个栈帧 / 删除当前栈帧 /“逆向运行”(Drop frame)

当我们在 Debug 时出现手抖等情况,提前或按错了下一步,导致错过了断点。此时可以通过 Idea 提供的 Drop Frame 功能,来返回到上一个栈帧

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)[插图] 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

其实不光是 Java,其他编程语言的方法执行模型,也是一个栈结构,方法的执行对应着一次 push/pop 的操作

比如下面这段代码,当执行过一次方法后,栈帧上有两个方法

debug调试多个java服务_java_04

debug调试多个java服务_spring_05

此时,点击 Drop Frame 按钮后,会删除栈顶上的数据,回到调用 log 方法前的位置

debug调试多个java服务_编程语言_06

注意:Drop Frame 虽然好用,但是可能在 Drop Frame 之后发生一些不可逆的问题,比如 IO 类的操作,或已修改的共享变量是无法回滚的,因为这个操作只是删除栈顶的栈帧,并不是真正的 “逆向运行”

强制方法返回(Force Return)

当一个方法比较长,或者 Step Info 到一个不太重要的方法想跳过该方法时,可以通过 Force Return 功能来强制结束该方法

debug调试多个java服务_多线程_07

注意:Force Return 和 Step Out 不一样,Step Out 是跳出当前步骤,还是会执行方法中的代码;而 Force Return 是直接强制结束方法, 跳过该方法后的所有代码直接返回。比如下面这段代码,当使用 Force Return 后,evaluate 方法中的 println 并不会执行

当要强制返回的方法有返回值时(非 void),force return 还需要指定一个返回值

debug调试多个java服务_编程语言_08

触发异常

当调用的方法可能抛出异常,调用者需要处理异常时,可以直接让方法抛出异常而不用修改代码

下面是一段伪代码,模拟发送请求,超时自动重试

debug调试多个java服务_多线程_09

当方法执行至 sendPacket 时,可以执行 Throw Exception 操作,提前结束方法并抛出指定的异常

debug调试多个java服务_编程语言_10

debug调试多个java服务_多线程_11

调用者收到异常后,就可以执行 catch 中的重试逻辑了,这样以来就不用通过修改程序等操作来模拟异常,非常的方便

Debug 运行中的 JVM 进程(Attach to Process)

当应用程序无法在 Idea 中运行,又想 Debug 这个运行中的程序时,可以通过 Attach to Process 功能,该功能可以 Debug 做到调试运行中的程序,当然前提是,保证这个正在运行的 JVM 进程代码和 Idea 中的代码一致

debug调试多个java服务_多线程_12

这种场景其实挺常见的,比如你要调试 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:

debug调试多个java服务_debug调试多个java服务_13

然后配置好 Host/Port,点击 Apply 保存即可

debug调试多个java服务_spring_14

最后,先启动远程的 JVM 进程,然后在 Idea 中已 Debug 来运行刚才配置的 Configuration 即可

debug调试多个java服务_debug调试多个java服务_15

小提示:远程调试下,由于有网络的开销,反应会比较慢,而且会导致远程程序的暂停,使用时请找一个没有人使用的环境

多线程下的调试

多线程程序是比较难写的,确切的说是很难调试,一个不小心就会因为线程安全的问题引起各种 Bug,并且这些 Bug 还可能很难复现。由于操作系统的线程调度是我们无法控制的,所以多线程程序的错误有很大的随机性,一旦出现问题很难找到;我们的程序可能在 99.99% 的情况下都是正常的,但是最后的 0.01% 也很可能造成严重的错误

线程安全的最常见问题就是竞争条件,当某些数据被多个线程同时修改时,就可能会发生线程安全问题

比如下面这个流程,正常情况下程序没问题

debug调试多个java服务_spring_16

当出现了竞争问题,单个线程的 read 和 write 操作之间,调度了其他线程,此时数据就会出错

debug调试多个java服务_编程语言_17

下面是一段示例代码,虽然共享数据 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 可以将挂起粒度设置为线程,而不是整个进程

debug调试多个java服务_debug调试多个java服务_18

Suspend 设置为 Thread 后,如下图所示,将断点打在 a.add 这一行,然后以 Debug 模式运行程序后,主线程和新建的线程都会挂在 addIfAbsent 方法中,我们可以在 Idea 中的 Debug 面板中切换线程

debug调试多个java服务_spring_19

此时,Main 线程和子线程都已经调用了 contains 方法,并都返回 false,挂起在 a.add 这一行,都准备将 17 添加到 a 中

debug调试多个java服务_编程语言_20

执行下一步后,Main 线程成功的将 17 添加到集合中

debug调试多个java服务_debug调试多个java服务_21

此时切换到 Thread-0 线程,还是挂在 a.add(x) 这一行,但是集合 a 中已经有元素 17 了,但时 Thread-0 线程还是会继续 add,add 之后集合 a 就出现了重复元素 17,导致程序出现了 bug

debug调试多个java服务_java_22

debug调试多个java服务_多线程_23

从上面的例子可以看出,在调试多线程程序的过程中,利用 Idea Debug 的 Suspend 功能,可以很方便的模拟多线程竞争的问题,这对于编写或调试多线程程序实在太方便了

作者:空无