Java中各种IDE的Debug功能,都是通过Java提供的 Java Platform Debugger Architecture (JPDA) 来实现的。
借助Debug功能,可以很方便的调试程序,快速的模拟/找到程序中的错误。
Interllij Idea的Debug功能上说虽然看起来和Eclipse差不多,但是在使用体验上,还是要比Eclipse好了不少。
Debug中,最常用的莫过于下一步,下一个断点(Breakpoint),查看运行中的值几个操作;但是除了这些IDE还提供了一些“高级”的功能,可以帮助我们更方便的进行调试
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功能,来返回到上一个栈帧
虚拟机栈描述的是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),force return还需要指定一个返回值
当调用的方法可能抛出异常,调用者需要处理异常时,可以直接让方法抛出异常而不用修改代码
下面是一段伪代码,模拟发送请求,超时自动重试
当方法执行至sendPacket时,可以执行Throw Exception操作,提前结束方法并抛出指定的异常
调用者收到异常后,就可以执行catch中的重试逻辑了,这样以来就不用通过修改程序等操作来模拟异常,非常的方便
当应用程序无法在Idea中运行,又想Debug这个运行中的程序时,可以通过Attach to Process功能,该功能可以Debug做到调试运行中的程序,当然前提是,保证这个正在运行的JVM进程代码和Idea中的代码一致
这种场景其实挺常见的,比如你要调试springboot executable jar时,或者调试tomcat源码等独立部署运行的进程,通过Attach to Process就非常方便了,可以做到用Idea之外的环境+Idea中的代码进行Debug
这种功能其实在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
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功能,可以很方便的模拟多线程竞争的问题,这对于编写或调试多线程程序实在太方便了