在软件开发过程中,调试是一项基本技能。调试的英文单词是 debug,意思是去除 bug。俗话说,编程就是制造 bug 的过程,所以调试的重要性毋庸置疑。如果能掌握调试技巧,就能快速定位代码中的 bug。要知道,能看懂代码并不代表能写出代码,能写出代码也不代表能很好地调试代码。要想写出无 bug 的代码,我们必须掌握一些基本的调试技巧。
工欲善其事,必先利其器。无论你的开发工具是 IntelliJ IDEA 还是 Eclipse,调试器都是标配。遇到有问题的程序时,利用好调试器的跟踪和断点技巧,可以快速定位问题原因。虽然说正确使用日志也可以方便定位线上问题,但是日志并不是调试工具,开发环境中不要把 System.out.println 当成调试手段,正确的做法是掌握调试器自带的调试技巧。
1.实用IDEA调试技巧
如果你是做 Java 开发的,相信你听说过 IntelliJ IDEA。我和大多数 Java 开发者一样,一开始也是用 Eclipse 进行开发,但是自从换用 IDEA 之后就离不开它了,成为了 IDEA 的忠实粉丝(不好意思,这是广告。。。)。
不得不说,来自捷克的软件公司JetBrains真是个良心企业虚拟机文件怎么恢复 虚拟机数据恢复软件教程,旗下所有产品都是精品,除了IDEA,还有WebStorm、PhpStorm、PyCharm等,风格都很类似,一些类似的快捷键包括调试技巧也是通用的。
打开IDEA的调试面板,如下图所示,大致可以分为五个部分:
1.1 单步跟踪
说到调试,很多人的第一反应很可能是一步一步的跟踪分析程序,其实 IDEA 提供了很多快捷键来帮助我们跟踪程序,可以列举如下小技巧:
在调试的时候,经常需要浏览分析代码,有时候浏览了好几个源文件,却找不到当前执行到哪了。很多人可能会使用 Navigate Back 来返回,虽然可以返回,但是可能需要多次点击返回按钮。相对而言,使用这个技巧更容易快速定位到调试器当前正在执行的代码行。
这是最基本的单步命令,每次执行一行代码,如果那行代码有方法,就直接跳过,可以说是真正的一步一步来。
Step Over 会跳过方法的执行,可以观察到方法的返回值。但是如果需要进入方法并观察方法的执行细节,则需要使用 Step In 命令。另外 Step In 命令还会跳过 JDK 自带的系统方法,如果要跟踪系统方法的执行细节,则需要使用 Force Step In 命令。
至于单步执行时忽略哪些系统方法,可以在IDEA的配置项Settings -> Build, Execution, Deployment -> Debugger -> Stepping中配置,如下图所示。
当使用Step In命令跟踪到某个方法中时,如果发现不想继续调用此方法,那么可以直接执行该方法,并停在下一行调用该方法的地方,这就是Step Out命令。
这一招可以说是调试器的杀手锏。单步调试的时候,如果因为粗心大意而步进过远,错过了关键代码的执行,比如想定位某个中间变量的值,如果能回到那行关键代码重新执行就好了。Drop to frame 就为我们提供了这个能力,它可以回到调用方法的地方(不像 Step Out 会回到方法调用的下一行),让我们可以重新调试这个方法。这次可不能大意了。
Drop to frame 的原理其实很简单,顾名思义就是删除栈顶的栈帧(也就是当前执行的方法),让程序回到上一个栈帧(父方法)。可以想象,这样只会恢复栈中的局部变量,全局变量是无法恢复的。如果方法中有对全局变量进行操作的地方,就没有办法再重新做了。
这两个命令在需要临时断点的时候非常有用。比如你已经知道要分析哪一行代码,但又不需要设置很多不必要的断点,那么就可以直接使用命令执行到某一行。Force Run to Cursor 甚至可以忽略所有断点,直接跳到我们要分析的地方。
1.2 断点管理
断点是调试器的基本功能之一,可以让程序在需要的地方暂停,帮助我们分析程序的运行过程。IDEA 中的断点管理如下图所示。正确使用断点技巧可以快速将程序停在我们想要停止的地方:
断点分为两种:行断点指的是暂停在特定的一行代码上,而全局断点则是在满足某个条件的时候才停止,并不局限于停在某一固定的行上,比如出现异常的时候就暂停程序。
1.2.1 行断点
条件断点。这是每个使用调试器的开发人员都应该掌握的技能。当你遇到很大的 List 或 Map 对象时,比如 1000 个 Person 对象,你不可能调试每一个对象。你可能只想在 person.name = 'Zhangsan' 时断点。你可以使用条件断点,如下图所示:
看到上面的 Suspend 选项,可能有人会觉得奇怪,设置断点的目的不就是为了停止程序吗?为什么要这个选项?是不是有点多余?设置断点不就是为了停止程序吗?
在我发现 evaluate 和 log 这个技巧之前,我也对此感到很奇怪,直到有一天我突然发现 Suspend Off + evaluate 和 log 这个组合确实很有用。正如之前所说,不要使用 System.out.println 作为调试方法,因为你可以使用这个技巧打印出所有你想打印的信息,而不需要修改你的源代码。
一次性断点。上面描述的“运行到光标”是一次性断点的示例。
我并不经常使用这些技巧,但它们是非常有用且值得记住的技巧。在 IDEA 中,每个对象都有一个实例 ID。实例过滤器用于在断点处的代码实例与设置的 ID 匹配时中断。传递计数用于在断点处暂停代码的执行。
1.2.2 全局断点
个人感觉这些技巧并不是很常用,有兴趣的同学可以自己尝试一下。
1.3 表达式求值
在一堆单步跟踪按钮旁边,有一个不起眼的按钮,就是“evaluate expression”。它在调试的时候非常有用,可以查看变量的值,计算表达式的值,甚至可以计算自己代码的值,它对应下面两种不同的模式:
这两种模式和Eclipse中的expression View和Display View类似,你也可以在Display View中编写一段代码执行,这个功能真的很强大,不过请注意这里只能编写代码片段,不能自定义方法,如下所示:
1.4 堆栈和线程
这个没什么好多说的,一个视图可以查看当前所有线程,另一个视图可以查看当前函数堆栈。在线程视图中可以进行thread dump,分析每个线程当前在做什么;在堆栈视图中可以切换堆栈帧,结合右侧的变量观察区,可以方便查看各个函数中的局部变量和参数。
1.5 变量观察
变量区和观察区可以合并显示,也可以分开显示(如下图),我比较喜欢分开显示,这样局部变量、参数、静态变量都显示在变量区,而需要观察的表达式则显示在观察区。
观察区类似求值表达式中的 expression 模式,可以添加需要观察的表达式,调试时实时看到表达式的值。变量区的内容相对固定,随着左侧堆栈框架的调整,值也会发生变化,也可以在这里修改变量的原始值。
2.使用jdb命令行调试
相信很多人都听说过gdb,可以说是调试的鼻祖,之前学习C/C++的时候就用过它调试程序。jdb和gdb一样,也是一个用来调试Java程序的命令行调试器。而且jdb不需要安装和下载,是JDK自带的工具(在JDK的bin目录下,而不是JRE里)。
每当我研究一个新技术的时候,我总会查看是否有可以替代它的命令行版本的工具。命令行操作给人一种安全感。每条指令、每个参数都清晰地列在那里。相比于图形界面工具,这可以让你学到更深层次的知识,而不是把技术细节藏在图形界面后面。你会发现命令行上的每一个参数、每一个配置都是可以学习的点。
2.1 jdb基本命令
在jdb中调试Java程序如下图所示。只需使用jdb Test命令加载程序即可。
运行完 jdb Test 命令之后,此时程序并没有运行,而是停在那里等待进一步的命令。此时我们可以想想在哪里设断点,比如在 main() 函数处设断点,然后使用 run 命令运行程序:
> stop in Test.main
正在延迟断点Test.main。
将在加载类后设置。
> run
运行Test
设置未捕获的 java.lang.Throwable
设置延迟的未捕获的 java.lang.Throwable
>
VM 已启动:设置延迟的断点:Test.main
可以看出,在执行run命令之前,程序还没有开始运行,此时的断点称为“延迟断点”。当程序真正运行时,也就是JVM启动时,才设置断点。除了stop in Class.Method命令外,还可以使用stop at Class:LineNumber方法来设置断点。
main[1] stop at Test:25
在jdb中设置断点没有IDEA中那么复杂,不支持条件断点和实例过滤器,只能一步一步地进行。在断点处,可以使用list命令查看断点附近的代码,也可以使用step命令单步执行,print或dump打印某个变量或表达式的值,locals命令查看当前方法中的所有变量,cont命令继续执行代码。
还有一些其他的命令我就不详细介绍了,大家可以使用help查看所有命令的列表,或者参考jdb的官方文档。
2.2 探索类文件结构
在jdb中调试Java程序的时候,有可能出现源代码文件和class文件不在一起的情况,这种情况下需要指定源代码位置:
# jdb -sourcepath path/to/source Test
如果不指定源代码位置,使用list命令时会提示找不到源代码文件。如果真的没有源代码文件,那你在jdb中就彻底瞎了。我们知道Java代码在执行时是以字节码的形式运行在JVM中的。我们可以猜测class文件肯定是有一些和源代码相关的信息,类似于C/C++语言的obj文件。否则list命令怎么能显示出当前正在执行的是哪一行代码呢?
我们可以使用开源的jclasslib软件来查看一个class文件的内容,一个标准的class文件包含以下信息:
如下图所示,其中最重要的部分就是Code属性,在Code属性下面是行号属性LineNumberTable,这个LineNumberTable就是调试器用来关联字节码和源代码的关键,对于class文件可以参考:
题外话:没有源代码如何调试?
如果没有源码的话,虽然可以使用jdb中的step来单步执行,但是没有办法显示当前运行的代码,简直就是盲调试。这时候就只能使用字节码调试工具了。常见的字节码调试器有:Bytecode Visualizer、JSwat Debugger、Java ByteCode Debugger(JBCD)等。可供参考:
3. 关于远程调试
通过对 jdb 的了解,我们离 Java 调试器的真相越来越近了,不过还有最后一步,我们先来看一下在 IDEA 中 Java 程序是如何调试的。如果你很好奇的话,在 IDEA 中调试程序的时候,你可能已经发现了以下秘密:
或者在调试Tomcat的时候,也有一串类似的、仿佛施了魔法般的参数:
这串神奇的参数看起来是这样的。一旦你理解了这串参数,你就会打破 Java 调试器的魔咒,然后你就能看到 Java 调试器的真面目:
"C:\Program Files\Java\jdk1.8.0_111\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n Foo
Connected to the target VM, address: '127.0.0.1:20060', transport: 'socket'
这里有两个关键点:
在IDEA的Run/Debug Configuration页面中,还可以添加远程调试器,界面如下图,可以发现上面的magic参数又出现了:
在我们真正开始远程调试之前,我们不妨带着这些问题来学习一下 Java 调试器的基本原理。
4. Java调试原理及JPDA介绍
在武术的世界里,武术可以分为两种:一种讲究招式新颖,出招时能出其不意,善于利用兵器的特点和自己高超的技巧,在敌人不备时出击;另一种讲究内功,哪怕是最普通的招式,结合自己深厚的内功,出招时也能威力雷霆。其实在技术的世界里,武术也可以分为技巧和原理两种。
上面说了这么多,不管你掌握了IDEA所有的调试技巧,还是记住了jdb的所有命令,都只是招式上的变化,不管怎么变,本质是一样的。接下来我们来看看调试器的内功。
4.1 联合药物开发协会
我们知道,Java程序都是运行在JVM上的,调试一个Java程序其实就是需要向JVM请求当前的运行状态,向JVM发出某些指令或者接收JVM的回调。为了调试Java程序,JVM提供了一整套调试工具和接口,我们把这套接口称为JPDA(Java Platform Debugger Architecture)。
这个体系给开发者提供了一套完整的调试Java程序的API,是一套用于开发Java调试工具的接口和协议,本质上就是我们访问虚拟机、检查虚拟机运行状态的一个通道和一套工具。
JPDA 由三个相对独立的层组成,并规定了它们之间的交互。这三层从低到高分别是 Java 虚拟机工具接口(JVMTI)、Java 调试线协议(JDWP)和 Java 调试接口(JDI),如下图所示(图片来自 IBM developerWorks):
这三个模块将调试过程分解成几个很自然的概念:调试器、被调试者以及它们之间的通信器。被调试者运行在我们要调试的Java虚拟机上,它可以通过JVMTI标准接口监视当前虚拟机的信息;调试器定义了用户可以使用的调试接口,通过这些接口用户可以向被调试者虚拟机发送调试命令,调试器接收并显示调试结果。
调试器与被调试者之间通过JDWP通信协议传输调试命令和调试结果。所有命令都封装成JDWP命令包,通过传输层发送给被调试者。被调试者收到JDWP命令包后解析命令并转换成JVMTI调用,在被调试者上运行。同样,JVMTI运行结果也被格式化成JDWP数据包,发送给调试器并返回给JDI调用。调试器开发者通过JDI获取数据并下发指令。
有关详细信息,请参阅
4.2 连接器和传输
至此我们了解到jdwp是调试器与被调试程序之间的通信协议,然而命令行参数-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n中的jdwp似乎不止这些。
其实这里的jdwp.dll库文件把JDI、JDWP、JVMTI连接成了一个整体,它不仅可以调用本地JVMTI提供的调试能力,还可以实现JDWP的通信协议,满足JVMTI与JDI之间的通信。
为了彻底理解这串参数的含义,我们还需要学习两个概念:Connectors(连接器)和Transport(传输)。
常见的连接器有五种,参考了JDWP建立连接的方式:
连接连接器与监听连接器的区别在于调试器或者被调试程序是否作为服务器。
传输是指JDWP的通信方式,一旦调试器和被调试程序之间建立了连接,它们就需要开始通信了。目前有两种通信方式:Socket和Shared-memory(仅在Windows平台使用)。
4.3 实际远程调试
通过上面的学习我们知道Java调试器与被调试程序是以C/S架构形式运行的,首先要启动一端作为服务器,然后连接另一端作为客户端。如果被调试程序以服务器方式运行,则需添加如下命令行参数:
# java -agentlib:jdwp=transport=dt_socket,server=y,address=5005 Test
# java -agentlib:jdwp=transport=dt_shmem,server=y,address=javadebug Test
第一句以套接字通信模式启动程序,第二句以共享内存模式启动程序。套接字模式需要指定调试器连接的端口号,共享内存模式需要指定连接名而不是端口号。
程序运行之后,可以使用jdb的-attach参数连接调试器和被调试程序:
# jdb -attach 5005
# jdb -attach javadebug
在 Windows 上,第一个命令会报错,类似这样的:java.io.IOException: shmembase_attach failed: 系统找不到指定的文件。这是因为 jdb -attach 使用系统默认传输来建立连接,而 Windows 上的默认传输是共享内存。在 Windows 上使用套接字方法连接,请使用 jdb -connect 命令虚拟机文件怎么恢复 虚拟机数据恢复软件教程,但命令参数不太好记:
# jdb -connect com.sun.jdi.SocketAttach:hostname=localhost,port=5005
# jdb -connect com.sun.jdi.SharedMemoryAttach:name=javadebug
另一方面,如果您希望调试器作为服务器运行,请执行以下命令:
# jdb -listen javadebug
然后,Java 程序使用以下参数连接到调试器:
# java -agentlib:jdwp=transport=dt_shmem,address=javadebug, suspend=y Test
# java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:5005,suspend=y,server=n
最后我们回顾一下IDEA打印出来的神奇参数,我们可以大胆猜测一下,在调试的时候,IDEA首先会将调试器作为服务器启动,并监听20060端口,然后Java程序以socket通信方式连接该端口并暂停JVM等待调试。
"C:\Program Files\Java\jdk1.8.0_111\bin\java" -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:20060,suspend=y,server=n Foo
Connected to the target VM, address: '127.0.0.1:20060', transport: 'socket'
如果您在 IDEA 下进行远程调试,可以参考 IBM developerWorks 上的另一个与调试相关的主题:使用 Eclipse 远程调试 Java 应用程序。
总结
本文首先介绍了IDEA的一些常用调试技巧,然后使用jdb调试Java程序并学习了常用的jdb命令。最后通过远程调试引入了调试器原理这一话题,并对JPDA、JVMTI、JDWP、JDI等概念进行了初步的了解。从技巧到心法,从技巧到原理,逐步揭开Java调试器的神秘面纱。
对于开发者来说,如果只懂招数,会一些奇葩的技巧,那么只能是比较熟练地使用工具,但很难在技术上取得质的突破;而如果只懂心法,埋头在基础原理和理论中,那么只能是一个志存高远、本事低下的学者,有一大堆大道理却无处安放。我们应该内外兼修,招数与心法相结合,融会贯通,才能取得成功。
最后,关于调试,我还要补充一句话:调试程序是一个费时费力的过程,一旦需要调试来定位问题,就说明代码的逻辑性和清晰度有问题。最好的代码是不需要调试的。所以,少调试,多单元测试,多重构,写出更清晰的代码才是最好的编程方法。更多IDEA使用技巧,在Java知音公众号回复“IDEA聚合”送你完整教程
补充:关于调试器的不确定性效应
在量子物理学中,有一个术语叫不确定性原理,又称不确定原理,指粒子的位置和动量不能同时确定,位置的不确定性越小,动量的不确定性越大,反之亦然。
简单来说,如果你想非常精确地测量一个粒子的位置,那么你就无法精确地测量这个粒子的动量;如果你想非常精确地测量一个粒子的动量,那么你就无法精确地测量这个粒子的位置;正是因为测量本身会给系统带来影响。
将这个现象应用到调试器领域,也有类似的效果。由于调试器本身的干扰,程序已经不再和以前一样了。那么问题来了,在调试器下运行的结果真的可以相信吗?
下面是我想出的一个有趣的例子。假设我们在第 4 行设置一个断点,程序的最终输出是什么?
(超过)
在公众号后台回复关键字“微信”即可免费获取建筑资料一份,添加群主微信即可免费加入高端建筑之路微信群,设为星星,并备注建筑之路。(公众号所有者禁止进入!)
最近的好文章
点击“关注”的你比别人好看多了!