乐趣区

关于IDEA:IDEA中Debug调试的高级武功秘籍助你快速追踪BUG

前言:

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 过程的性能:(留神:IDEA 中装置了 Java Stream Debugger 插件才反对此性能)

批改程序执行流程:

在 Debug 调试的过程中,个别状况下,让程序失常执行即可。

然而某些状况下,须要动静的批改执行流程,此时如果通过批改代码再从新调试的形式还是太不不便了,好在 Idea 提供了一些动静批改程序执行流程的性能,能够让咱们很灵便的进行调试。

1、返回上一个栈帧 / 删除以后栈帧 /“逆向运行”(Drop frame):

当咱们在 Debug 时呈现手抖等状况,提前或按错了下一步,导致错过了断点。此时能够通过 Idea 提供的 Drop Frame 性能,来返回到上一个栈帧:

虚拟机栈形容的是 Java 办法执行的内存模型:每个办法在执行的同时都会创立一个栈帧(Stack Frame)[插图] 用于存储局部变量表、操作数栈、动静链接、办法进口等信息。

每一个办法从调用直至执行实现的过程,就对应着一个个栈帧在虚拟机栈中入栈到出栈的过程。

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

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

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

留神:Drop Frame 尽管好用,然而可能在 Drop Frame 之后产生一些不可逆的问题,比方 IO 类的操作,或已批改的共享变量是无奈回滚的,因为这个操作只是删除栈顶的栈帧,并不是真正的“逆向运行”

2、强制办法返回(Force Return):

当一个办法比拟长,或者 Step Info 到一个不太重要的办法想跳过该办法时,能够通过 Force Return 性能来强制完结该办法:

留神:Force Return 和 Step Out 不一样,Step Out 是跳出以后步骤,还是会执行办法中的代码;而 Force Return 是间接强制完结办法,跳过该办法后的所有代码间接返回。比方上面这段代码,当应用 Force Return 后,evaluate 办法中的 println 并不会执行

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

3、触发异样:

当调用的办法可能抛出异样,调用者须要解决异样时,能够间接让办法抛出异样而不必批改代码;

上面是一段伪代码,模仿发送申请,超时主动重试:

当办法执行至 sendPacket 时,能够执行 Throw Exception 操作,提前结束办法并抛出指定的异样:

调用者收到异样后,就能够执行 catch 中的重试逻辑了,这样以来就不必通过批改程序等操作来模仿异样,十分的不便。

4、计算表达式:

计算表达式:指的是在调试时,能够将曾经在调试中失去的后果,通过计算表达式进行动静解决,而不必去批改代码从新再调试;

上面举个例子形容下具体操作步骤:

编写的测试伪代码:

而后在调试的 variables 区域右击鼠标,点击 evaluate expression

而后呈现计算表达式框:留神 getString() 办法是曾经在代码中写好了的,并且 办法参数 bytes 是下面调试的代码得出的数据,最初点击 evaluate 执行表达式就会失去后果

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 才连贯,早曾经错过了断点的机会。

而后配置好 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 学习干货文章,您能够来瞧一瞧哟!

参考资料:

①、Java Platform Debugger Architecture (JPDA) | Oracle Docs

②、Debugging Applications | Oracle Docs

③、Debug code – Help | IntelliJ IDEA

退出移动版