当前位置:首页 > 学术指导

高效开发:IntelliJ IDEA天天用,这些Debug技巧你都知道?

在软件开发的过程中,可以说调试是一项基本技能。调试的英文单词为debug,顾名思义,就是去除bug的意思。俗话说的好,编程就是制造bug的过程,所以debug的重要性毋庸置疑,如果能熟练掌握调试技能,也就可以很快的定位出代码中的bug。要知道,看的懂代码不一定写的出代码,写的出代码不一定能调试好代码,为了能写出没有bug的代码,我们必须得掌握一些基本的调试技巧。

工欲善其事,必先利其器。无论你的开发工具是IntelliJIDEA还是Eclipse,调试器都是标配。在遇到有问题的程序时,合理的利用调试器的跟踪和断点技巧,可以很快的定位出问题原因。虽然说合理利用日志也可以方便定位线上问题,但是日志并不是调试工具,不要在开发环境把当作调试手段,掌握调试器自带的调试技能才是正道。

一、实战IDEA调试技巧

如果你是做Java开发的,相信你不会没有听过IntelliJIDEA,和大多数Java开发者一样,我一开始的时候也是用Eclipse来进行开发,但是自从换了IDEA之后,就再也离不开它了,彻底变成了IDEA的忠实粉丝(不好意思,打一波广告。。)。

不得不说,JetBrains这家来自捷克的软件公司真的是良心企业,所出产品皆是精品,除了IDEA,还有WebStorm,PhpStorm,PyCharm等,风格都是很类似的,一些类似的快捷键包括调试技巧也是通用的。

打开IDEA的调试面板,如下图所示,可以大致的将其分成五个部分:

单步跟踪

断点管理

求值表达式

堆栈和线程

变量观察


1.1单步跟踪

说起调试,估计很多人第一反应就是对程序进行一步一步的跟踪分析,其实IDEA提供了很多快捷键来帮助我们跟踪程序,大抵可以列出下面几个技巧:

ShowExecutionPoint

调试时往往需要浏览代码,对代码进行分析,有时候在浏览若干个源文件之后就找不到当前执行到哪了,可能很多人会使用NavigateBack来返回,虽然也可以返回去,但可能需要点多次返回按钮,相对来说使用这个技巧快速定位到当前调试器正在执行的代码行要更简便。

StepOver

这是最基本的单步命令,每一次都是执行一行代码,如果该行代码有方法会直接跳过,可以说真的是一步一个脚印。

StepIn/ForceStepIn

StepOver会跳过方法的执行,可以观察方法的返回值,但如果需要进到方法里面,观察方法的执行细节,则需要使用StepIn命令了。另外,StepIn命令也会跳过jdk自带的系统方法,如果要跟踪系统方法的执行细节,需要使用ForceStepIn命令。

关于单步的时候忽略哪些系统方法,可以在IDEA的配置项Settings-Build,Execution,Deployment-Debugger-Stepping中进行配置,如下图所示。


StepOut

当使用StepIn命令跟踪到一个方法的内部时,如果发现自己不想继续调这个方法了,可以直接把这个方法执行完,并停在调用该方法的下一行位置,这就是StepOut命令。

DroptoFrame

这一招可以说是调试器的一大杀器。在单步调试的时候,如果由于粗心导致单步过了头,没有看到关键代码的执行情况,譬如想定位下某个中间变量的值,这个时候如果能回到那行关键代码再执行一遍就好了,DroptoFrame就提供了我们这个能力,它可以回到方法调用的地方(跟StepOut不一样,StepOut是回到方法调用的下一行),让我们可以再调试一次这个方法,这一次可不要再粗心了。

Droptoframe的原理其实也非常简单,顾名思义,它将堆栈的最上面一个栈帧删除(也就是当前正在执行的方法),让程序回到上一个栈帧(父方法),可以想见,这只会恢复堆栈中的局部变量,全局变量无法恢复,如果方法中有对全局变量进行操作的地方,是没有办法再来一遍的。

RuntoCursor/ForceRuntoCursor

这两个命令在需要临时断点时非常有用,譬如已经知道自己想分析哪一行代码了,但又不需要下很多无谓的断点,可以直接使用该命令执行到某行,ForceRuntoCursor甚至可以无视所有断点,直接到达我们想分析的地方。

1.2断点管理

断点是调试器的基础功能之一,可以让程序暂停在需要的地方,帮助我们进行分析程序的运行过程。在IDEA中断点管理如下图所示,合理使用断点技巧可以快速让程序停在我们想停的地方:


可以将断点分成两种类型:行断点指的是在特定的某行代码上暂停下来,而全局断点是在某个条件满足时停下来,并不限于停在固定的某一行,譬如当出现异常时暂停程序。

1.2.1行断点

Susp(All/Thread)

Condition

条件断点。这应该也是每个使用调试器的开发者都应该掌握的一个技巧,当遇到遍历比较大的List或Map对象时,譬如有1000个Person对象,你不可能每个对象都调一遍,你可能只想在='Zhangsan'的时候让断点断下来,就可以使用条件断点,如下图所示:


Logmessagetoconsole

Evaluateandlog

当看到上面的Susp这个选项的时候有的人可能会感到奇怪,我下一个断点不就是为了让程序停下来吗?还需要这个选项干什么?是不是有点多余?难道你下个断点却不想让程序停下来?

在发现Evaluateandlog这个技巧之前,我对这一点也感觉很奇怪,直到有一天我突然发现SuspOff+Evaluateandlog的配合真的是太有用了。前面有讲过,不要把当作调试手段,因为你完全可以用这个技巧来打印所有你想打印的信息,而不需要修改你的源代码。

Removeoncehit

一次性断点。上面介绍的RuntoCursor就是一次性断点的例子。

Instancefilters

Classfilters

Passcount

这几个我用的不是很多,但应该也是非常有用的技巧可以先记下来。在IDEA里每个对象都有一个实例ID,Instancefilters就是用于当断点处代码所处的实例和设定ID匹配则断下来。Passcount则是在断点执行到第几次的时候暂停下来。

1.2.2全局断点

Exceptionbreakpoints

Methodbreakpoints

Fieldwatchpoints

个人感觉这几个技巧都不是很常用,感兴趣的同学自己实验一把吧。

1.3求值表达式

在一堆单步跟踪的按钮旁边,有一个不显眼的按钮,这个按钮就是“求值表达式”。它在调试的时候很有用,可以查看某个变量的值,也可以计算某个表达式的值,甚至还可以计算自己的一段代码的值,这分别对应下面两种不同的模式:

表达式模式(ExpressionMode)

代码片段模式(CodeFragmentMode)

这两个模式类似于Eclipse里面的ExpressionView和DisplayView。在DisplayView里也可以编写一段代码来执行,确实非常强大,但是要注意的是,这里只能写代码片段,不能自定义方法,如下图:


1.4堆栈和线程

这个没什么好说的,一个视图可以查看当前的所有线程,另一个视图可以查看当前的函数堆栈。在线程视图里可以进行Threaddump,分析每个线程当前正在做什么;堆栈视图里可以切换栈帧,结合右边的变量观察区,可以方便的查看每个函数里的局部变量和参数。

线程视图

堆栈视图


1.5变量观察

变量区和观察区可以合并在一起,也可以分开来显示(如下图所示),我比较喜欢分开来显示,这样局部变量、参数以及静态变量显示在变量区,要观察的表达式显示在观察区。

观察区类似于求值表达式中的Expressionmode,你可以添加需要观察的表达式,在调试的时候可以实时的看到表达式的值。变量区的内容相对是固定的,随着左边的栈帧调整,值也会变得不同。在这里还可以修改变量原有的值。


二、使用jdb命令行调试

相信很多人都听过gdb,这可以说是调试界的鼻祖,以前在学习C/C++的时候,就是使用它来调试程序的。和gdb一样,jdb也是一个命令行版的调试器,用于调试Java程序。而且jdb不需要安装下载,它是JDK自带的工具(在JDK的bin目录中,JRE中没有)。IDEA更多玩法:IntelliJIDEA内容聚合

每研究一项新技术,我总是会看看有没有命令行版本的工具可以替代,在命令行下进行操作给人一种踏实的感觉,每一个指令,每一个参数,都清清楚楚的摆在那里,这相比较于图形界面的工具,可以学习更深层的知识,而不是把技术细节隐藏在图形界面之后,你可以发现命令行下的每一个参数,每一个配置,都是可以学习的点。

2.1jdb基本命令

在jdb中调试Java程序如下图所示,直接使用jdbTest命令加载程序即可。


运行完jdbTest命令之后,程序这时并没有运行起来,而是停在那里等待进一步的命令。这个时候我们可以想好在哪里下个断点,譬如在main()函数处下个断点,然后再使用run命令运行程序:

正在延迟断点。将在加载类后设置。run运行Test设置未捕获的设置延迟的未捕获的已启动:设置延迟的断点:

可以看出在执行run命令之前,程序都还没有开始运行,这个时候的断点叫做“延迟断点”,当程序真正运行起来时,也就是JVM启动的时候,才将断点设置上。除了命令,还可以使用stopatClass:LineNumber的方式来设置断点。

main[1]stopatTest:25

在jdb中下断点,就没有IDEA中那么多名堂了,什么条件断点,什么Instancefilters都不支持,只能乖乖的一步一步来。在断点处,可以使用list命令查看断点附近的代码,或者用step命令单步执行,print或者dump打印变量或表达式的值,locals命令查看当前方法中的所有变量,cont命令继续执行代码。

还有一些其他的命令就不多做介绍了,可以使用help查看所有的命令清单,或者参考jdb的官方文档。IDEA更多玩法:IntelliJIDEA内容聚合

2.2探索class文件结构

在jdb中调试Java程序时,有可能源代码文件和class文件不在一起,这个时候需要指定源码位置:

java-agentlib:jdwp=transport=dt_socket,server=y,address=5005:hostname=localhost,port=5005jdb-listenjavadebug

然后Java程序通过下面的参数来连接调试器:

java-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:5005,susp=y,server=n

最后我们再回过头来看一看IDEA打印出来的这串魔咒参数,可以大胆的猜测,IDEA在调试的时候,首先以服务器形式启动调试器,并在20060端口监听,然后Java程序以socket通信方式连接该端口,并将JVM暂停等待调试。

"C:\ProgramFiles\Java\_111\bin\java"-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,susp=y,server=nFooConnectedtothetargetVM,address:'127.0.0.1:20060',transport:'socket'

如果在IDEA下进行远程调试,可以参考IBMdeveloperWorks上的另一篇与调试相关的主题:使用Eclipse远程调试Java应用程序。

总结

这篇文章首先介绍了IDEA的一些常用调试技巧,然后通过使用jdb进行Java程序的调试,学习了jdb的常用命令,最后通过远程调试引出调试器原理的话题,对JPDA、JVMTI、JDWP、JDI等概念有了一个初步的认识。从招式到心法,由技巧到原理,逐步揭开了Java调试器的神秘面纱。

对于开发人员来说,如果只懂招式,只会一些奇淫技巧,那么他只是把工具用得更得心应手而已,很难在技术上得到质的突破;而如果只懂心法,只沉浸于基本原理和理论,那么他只能做一个眼高手低的学院派,空有满腹大道理却无用武之地。我们更应该内外兼修,把招式和心法结合起来,融会贯通,方能成正果。

最后的最后,关于调试的话题不得不补充一句:调试程序是一个费时费力的过程,一旦需要调试来定位问题,说明代码的逻辑性和清晰性有问题,最好的代码是不需要调试的。所以,少一点调试,多一点单元测试,多一点重构,将代码写的更清晰才是最好的编程方式。更多IDEA使用技巧,Java知音公众号内回复“IDEA聚合”,送你一份完整教程

番外篇:关于调试器的测不准效应

在量子物理学中,有一个名词叫测不准原理,又叫不确定性原理,讲的是粒子的位置与动量不可同时被确定,位置的不确定性越小,则动量的不确定性越大,反之亦然。

说白点就是,你如果要很准确的测量粒子的位置,那么就不能准确的测量粒子的动量;如果要很准确的测量粒子的动量,那么粒子的位置就测不准;正是由于测量本身,会导致系统受影响。

把这个现象套在调试器领域里,也有着类似的效果。由于调试器本身的干扰,程序已经不是以前的程序了。所以问题来了,在调试器下运行出来的结果,真的可信吗?

下面是我想出来的一个有趣的例子,假设我们在第4行下一个断点,程序最后输出结果会是什么呢?


分享到: