前言
Debug Java程序是Java程序员问题排查最有效的方式之一,在开发或测试程序时,通常都会对程序进行debug操作。常用的IDE比如IntelliJ IDEA 和Eclipse都提供了完备的调试工具供开发者使用。不过一般我们只和IDE打交道,对调试的细节很多人并不清楚,比如:
- 不借助IDE我们可以对程序进行调试吗?
- 我们通常是本地调试,那可以调试远端代码吗?
- 本地调试和远程调试有什么异同?它们都是怎么实现的?
读完本篇文章,我们会得到答案。
一、使用调试命令jdb对程序进行调试
我们知道jvm提供很多命令行工具,常用的如下表,其中的jdb就是进行调试的工具, jdb不但是个好的调试工具,也是一个好的学习工具,可以让我们了解程序的动态执行过程。 本小节我们使用jdb对简单的一段程序(代码见DisplayJdb.java)进行调试
命令 | 描述 |
---|---|
jinfo | 可以输出并修改运行时的java 进程的opts。 |
jdb | 命令行调试工具 |
jps | 列出所有Java进程的PID |
jstack | 列出虚拟机进程的所有线程运行状态 |
jmap | 列出堆内存上的对象状态 |
jstat | 记录虚拟机运行的状态,监控性能 |
jconsole | 虚拟机性能/状态检查可视化工具 |
DisplayJdb.java
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;
/**
* @author laughitover
*/
public class DisplayJdb {
public static void display() {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("DateTime :" + dateTimeFormatter.format(localDateTime));
int year = localDateTime.getYear();
int second = localDateTime.getSecond();
int sum = add(year, x -> x + second);
System.out.println(sum);
sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
System.out.println(sum);
}
public static int add(int i, Function<Integer, Integer> fun) {
return fun.apply(i);
}
public static void main(String[] args) {
display();
}
}
1. 编译java文件
使用如下命令对DisplayJdb.java文件进行编译, 其中,-g参数是为了产生各种调试信息,一定要加上,否则无法调试。-d . 是编译到当前目录下
javac -g -d . DisplayJdb.java
2. 使用jdb命令进入调试模式
执行命令jdb DisplayJdb
开始调试程序,我们可以用help
先看看jdb都有哪些调试命令,如下图:
3. 开始调试
- stop in DisplayJdb.display 把断点设在display函数上
- run 开始运行程序,
- locals 查看本地变量,(可以看到当前没有本地变量,因为程序才刚开始执行)
- list 查看运行到了源代码的什么位置,
- step 单步执行程序,
4. 加快执行
单步执行太慢了,可以使用 stop at DisplayJdb:17
在17行处下个断点,
然后使用cont
执行到下一断点处,使用stop
命令可以查看当前断点情况
5. 方法执行
接下来是add方法,可以使用step
一步一步执行并且会进入方法体,也可以使用next
命令把方法执行完。
6. 退出
接下来是一个for循环,可以使用 cont
执行结束自动推出,
也可以使用 exit
或 quit
命令退出
cont是continue的缩写,功能是运行到下一个断点处停止。
二、远程调试
Java程序一般是部署在linux服务器上的,因此我们有时需要对服务器上的程序进行远程debug。 其实我们只需在启动时加上相应参数即可实现该功能。
1. 创建一个简单的演示项目example
2. 用maven打包放到linux服务器上
3. 用以下命令启动example
java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=9998 example-0.0.1-SNAPSHOT.jar >> ./test.out &
- -Xdebug : 启用调试,即通知JVM工作在DEBUG模式下;
- -Xrunjdwp : 通知JVM使用JDWP(是一个通讯协议,下文会详细介绍)协议来运行调试环境,以下是子选项
- transport=dt_socket :指定调试数据的传送方式,dt_socket是指用SOCKET模式,另有dt_shmem指用共享内存方式,dt_shmem只适用于Windows平台。
- address=9998 :调试服务器的端口号,客户端用此端口号来连接服务器。
- server=y :y表示启动的JVM是被调试者。如果为n,则表示启动的JVM是调试器。
- suspend=y : 是否在调试客户端建立连接之后启动 VM
4. 在本地idea上创建远程连接,并以debug方式启动
5. 向本地项目一样打断点对程序进行调试
三、Java 调试原理
我们知道java程序都是运行在JVM上的,我们要调试Java程序,事实上就需要向Java虚拟机请求当前运行的状态,并对虚拟机发出一定的指令, 设置一些回调等等,由此推断,debug的过程肯定是JVM对此有专门的设计,这个设计就是JPDA
3.1、Java调试体系JPDA简介
Java虚拟机设计了专门的API接口供调试和监控虚拟机使用,被称为Java平台调试体系即Java Platform Debugger Architecture(JPDA)。
3.2、Java调试体系组成
JPDA按照抽象层次,又分为三层,分别是
- 3.2.1、JVM TI : Java VM Tool Interface,虚拟机对外暴露的接口,包括debug和profile
- 3.2.2、JDWP : Java Debug Wire Protocol,调试器和应用之间通信的协议
- 3.2.3、JDI : Java Debug Interface,Java库接口,实现了JDWP协议的客户端,调试器可以用来和远程被调试应用通信
JPDA是一个典型的C/S应用,可以和HTTP做类比 IDE+JDI = 浏览器 JDWP = HTTP JVMTI = RESTful接口 Debugee虚拟机= REST服务端
和其他的Java模块一样,Java只定义了Spec规范,也提供了参考实现(ReferenceImplementation),但是第三方完全可以参照这个规范, 按照自己的需要去实现其中任意一个组件,比如Eclipse就没有用Sun/Oracle的JDI,而是自己实现了一套, 它的两个插件org.eclipse.jdt.debug.ui和org.eclipse.jdt.debug与其强大的调试功能密切相关, 其中org.eclipse.jdt.debug.ui是Eclipse调试工具界面的实现,而org.eclipse.jdt.debug则是JDI的一个完整实现。
3.3、Java 虚拟机工具接口(JVMTI)
JVMTI(JavaVirtualMachineToolInterface)即指Java虚拟机工具接口,它是一套由虚拟机直接提供的native接口,它处于整个JPDA体系的最底层, 所有调试功能本质上都需要通过JVMTI来提供。通过这些接口,开发人员不仅可以调试该虚拟机上运行的Java程序,还能进行查看它们运行的状态, 设置回调函数,控制某些环境变量等操作,从而优化程序性能。
一般我们可以采用通过建立一个Agent的方式来使用JVMTI,可以在java程序启动的时候(增加启动参数agentlib/agentpath)来加载它, java5之后也可以在运行时加载,Idea远程调用使用的就是运行时加载(见下图)。其显著的特征就是通过设置回调函数的方式, 从java虚拟机上得到当前运行态信息,并做出自己的相应的操作,抑或操作虚拟机的运行态,以达到一些特定的目的。
JVMTI的前身是JVMDI和JVMPI,它们原来分别被用于提供调试Java程序以及Java程序调节性能的功能。JavaSE7后被JVMTI取代。
3.4、Java 调试线协议(JDWP)
JDWP(JavaDebugWireProtocol)是一个为Java调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。 在JPDA体系中,作为调试者(debugger)进程和被调试程序(debuggee)进程之间的交互数据的格式就是由JDWP来描述的, 它详细完整地定义了请求命令、回应数据和错误代码,保证了JVMTI和JDI的通信通畅。比如在Sun公司提供的实现中, 它提供了一个名为jdwp.dll(jdwp.so)的动态链接库文件,这个动态库文件实现了一个Agent, 它会负责解析JDI发出的请求或者命令,并将其转化为JVMTI调用,然后将JVMTI函数的返回值封装成JDWP据发还给JDI。
另外,这里需要注意的是JDWP本身并不包括传输层的实现,传输层需要独立实现,但是JDWP包括了和传输层交互的严格的定义, 就是说,JDWP协议虽然不规定我们是通过EMS还是快递运送货物的,但是它规定了我们传送的货物的摆放的方式。在Sun公司提供的JDK中, 在传输层上,它提供了socket方式,以及在Windows上的sharedmemory方式(远程调试对参数有介绍)。 当然,传输层本身无非就是本机内进程间通信方式和远端通信方式,所以本质上本地debug和远程debug原理是一样的,只是通信方式不同。
3.5、Java 调试接口(JDI)
JDI(JavaDebugInterface)是三个模块中最高层的接口,在多数的JDK中,它是由Java语言实现的。通过它, 调试工具开发人员就能通过本地虚拟机上的调试器来操纵远程虚拟机上运行的程序,JDI不仅能帮助开发人员格式化JDWP数据, 而且还能为JDWP数据传输提供队列、缓存等优化服务。从理论上说,开发人员只需使用JDWP和JVMTI即可支持跨平台的远程调试, 但是直接编写JDWP程序费时费力,而且效率不高。因此基于Java的JDI层的引入,简化了操作,提高了开发人员开发调试程序的效率。
链接是Debugger与TargetJVM之间交互的渠道,一个调试器可以链接多个目标虚拟机,但一个目标虚拟机最多只能链接一个调试器。 链接是由链接器(Connector)生成的,不同的链接器有着不同的实现方式。JDI中定义了三种链接器接口, 分别是依附型链接器(AttachingConnector)、监听型链接器(ListeningConnector)和启动型链接器(LaunchingConnector)。 在调试过程中,实际使用的链接器必须实现其中一种接口,而在虚拟机管理器中就提供了各种连接器的实现。
根据调试器在链接过程中扮演的角色,也可以将链接方式划分为主动链接和被动链接。主动链接表示调试器主动地向目标虚拟机发起链接。 被动链接表示调试器将被动地等待或者监听由目标虚拟机发起的链接。上文远程通过idea远程调试,用的就是主动链接
3.6回头看前言中的问题。
- 不借助IDE我们可以对程序进行调试吗?
从上文第一节可知,我们完全可以通过jdb直接对程序进行调试。 只是IDE实现了debug的ui,根据JPDA的一套接口实现了带界面debug流程。
- 我们通常是本地调试,那可以调试远端代码吗?
从上文第二节可知,我们完全可以调试远端代码,本质上和本地调试没什么区别
- 本地调试和远程调试有什么异同?它们都是怎么实现的?
其实本地和远程debug都是基于JPDA,并且默认在JDWP层都是使用socket通信,只不过本地方式下链接的targetVM为localhost, 远程debug连接的targetVM为远端机器。这个可以从启动参数看到,以直接在IDE中点击debug按钮为例,在debug时都会输出如下日志:
ConnectedtothetargetVM,address:’ip地址:端口号’,transport:’socket’
总结
本篇文章,我们从jdb命令和远程调试实践入手,系统全面的讲解了Java平台调试体系,我们可以看到整个JDPA有非常清晰的分层, 各司其职,让整个调式过程简单可以扩展,而这一切其实都是构建在高司令巨牛逼的Java虚拟机抽象之上的,由于规范的灵活性, 如果有特殊需求,完全可以自己去重新实现和扩展,例如,我们可以通过agent去加密解密加载的类,保护知识产权;我们可以记录虚拟机运行过程, 作为自动化测试用例;我们还可以把线上问题的诊断实践自动化下来,做一个快速预判,争取最宝贵的时间。
总之,通过JPDA这个标准,我们可以从虚拟机中得到我们所需要的信息,完成我们所希望的操作,更好地开发我们的程序。 而且,Java调试工具是建立在强大的虚拟机上的,因此,很多前沿的应用,比如动态编译运行,字节码的实时替换等等, 都可以通过对虚拟机的改进而得到实现。随着虚拟机技术的逐步发展和深入,各种不同种类,不同应用领域中虚拟机的出现, 各种强大的功能的加入,给我们的调试工具也带来很多新的应用。
演示源码地址:https://github.com/laughitover/example.git
参考文章: https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/index.html https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jdb.html https://www.cnblogs.com/rocedu/p/6371262.html https://yq.aliyun.com/articles/56?spm=5176.100238.yqhn2.7.NLnzoh https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr011.html