转载

你真的会DEBUG吗?

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吗?

你真的会DEBUG吗?

你真的会DEBUG吗?

修改程序执行流程

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

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

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

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

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

你真的会DEBUG吗?

你真的会DEBUG吗?

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

你真的会DEBUG吗?

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

强制方法返回(Force Return)

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

你真的会DEBUG吗?

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

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

你真的会DEBUG吗?

触发异常

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

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

你真的会DEBUG吗?

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

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

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

你真的会DEBUG吗?

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

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

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

多线程下的调试

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

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

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

下面是一段示例代码,虽然共享数据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吗?

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

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

你真的会DEBUG吗?

你真的会DEBUG吗?

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

参考

  • Java Platform Debugger Architecture (JPDA) | Oracle Docs
  • Debugging Applications | Oracle Docs
  • Debug code - Help | IntelliJ IDEA
原文  https://segmentfault.com/a/1190000023020733
正文到此结束
Loading...