原理
Java远程调试的原理是两个VM之间通过debug协议进行通信,然后以达到远程调试的目的,两者之间可以通过socket进行通信
调试体系JPDA
JPDA(Java Platform Debugger Architecture)是 sun 公司开发的 java平台调试体系,它主要有三个层次组成,即 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI)
JVMTI(JVMDI): jdk1.4 之前称为JVMDI,之后改为了JVMTI,它是虚拟机的本地接口,其相当于 Thread 的 sleep、yield native 方法
JDWP(Java Debug Wire Protocol):java调试网络协议,其描述了调试信息的格式,以及在被调试的进程(server)和调试器(client)之间传输的请求
JDI:java调试接口,虚拟机的高级接口,调试器(client)自己实现 JDI 接口,比如 idea、eclipse 等
下面使用两张图更直观的了解JPDA的三个模块层次
1、JPDA模块层次
2、JPDA层次比较
idea 或者 eclipse 调试原理
当我们在 idea 或者 eclipse 中以 debug 模式启动运行类,就可以直接调试了,这其中的原理令人不解,下面就给大家介绍一下
客户端(idea 、eclipse 等)之所以可以进行调试,是由于客户端 和 服务端(程序端)进行了 socket 通信,通信过程如下:
1、先建立起了 socket 连接
2、将断点位置创建了断点事件通过 JDI 接口传给了 服务端(程序端)的 VM,VM 调用 suspend 将 VM 挂起
3、VM 挂起之后将客户端需要获取的 VM 信息返回给客户端,返回之后 VM resume 恢复其运行状态
4、客户端获取到 VM 返回的信息之后可以通过不同的方式展示给客户
上述过程便是一个完整的 debug 调试过程,下面通过示例来进一步说明一下这个过程
使用 idea debug 调试一个类,过程如下图:
idea 和 程序之间建立了 socket 连接,ip 是 本机,端口是 52690,注意这个端口不是固定的,每次都会变动
cmd 中使用 netstat -ano | findstr 52690 查看该监听端口 52690 使用的进程
上图可以看出,idea 调试客户端 进程id 是 5472,程序调试服务器端 进程id 是 27600,两者之间建立了连接进行通信
服务端之所以可以和客户端建立起连接,是由于调试服务器端加了一句话,打开了调试,如下图:
cmd 中 使用 jps -v | findstr HelloWorld 查找进程信息
上图看出,HelloWorld 程序的进程id 确实是 27600,并在启动时添加了以下这句话:
-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52690,suspend=y,server=n
vm 挂起后将调试信息返回给客户端,客户端可以展示给用户,如下图:
debug调试示例demo
我们下面使用示例代码来调试运行一下某行代码的某个变量值,如下图:
1、新建调试程序代码
package com.demo.debug;
public class HelloWorld {
public static void main(String[] args) {
String str = "Hello world!";
System.out.println(str);
}
}
2、调试程序客户端代码
package com.demo.debug;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.StringReference;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.LaunchingConnector;
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.ClassPrepareEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventIterator;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.event.VMDisconnectEvent;
import com.sun.jdi.event.VMStartEvent;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.ClassPrepareRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import java.util.List;
import java.util.Map;
public class SimpleDebugger {
static VirtualMachine vm;
static Process process;
static EventRequestManager eventRequestManager;
static EventQueue eventQueue;
static EventSet eventSet;
static boolean vmExit = false;
public static void main(String[] args) throws Exception {
LaunchingConnector launchingConnector
= Bootstrap.virtualMachineManager().defaultConnector();
// Get arguments of the launching connector
Map<String, Connector.Argument> defaultArguments
= launchingConnector.defaultArguments();
Connector.Argument mainArg = defaultArguments.get("main");
Connector.Argument suspendArg = defaultArguments.get("suspend");
// Set class of main method
mainArg.setValue("com.demo.debug.HelloWorld");
suspendArg.setValue("true");
vm = launchingConnector.launch(defaultArguments);
process = vm.process();
// Register ClassPrepareRequest
eventRequestManager = vm.eventRequestManager();
ClassPrepareRequest classPrepareRequest
= eventRequestManager.createClassPrepareRequest();
classPrepareRequest.addClassFilter("com.demo.debug.HelloWorld");
classPrepareRequest.addCountFilter(1);
classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
classPrepareRequest.enable();
// Enter event loop
eventLoop();
process.destroy();
}
private static void eventLoop() throws Exception {
eventQueue = vm.eventQueue();
while (true) {
if (vmExit == true) {
break;
}
eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event = (Event) eventIterator.next();
execute(event);
}
}
}
private static void execute(Event event) throws Exception {
if (event instanceof VMStartEvent) {
System.out.println("VM started");
eventSet.resume();
} else if (event instanceof ClassPrepareEvent) {
ClassPrepareEvent classPrepareEvent = (ClassPrepareEvent) event;
String mainClassName = classPrepareEvent.referenceType().name();
if (mainClassName.equals("com.demo.debug.HelloWorld")) {
System.out.println("Class " + mainClassName
+ " is already prepared");
}
if (true) {
// Get location
ReferenceType referenceType = classPrepareEvent.referenceType();
List locations = referenceType.locationsOfLine(10);
Location location = (Location) locations.get(0);
// Create BreakpointEvent
BreakpointRequest breakpointRequest = eventRequestManager
.createBreakpointRequest(location);
breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
breakpointRequest.enable();
}
eventSet.resume();
} else if (event instanceof BreakpointEvent) {
System.out.println("Reach line 10 of com.demo.debug.HelloWorld");
BreakpointEvent breakpointEvent = (BreakpointEvent) event;
ThreadReference threadReference = breakpointEvent.thread();
StackFrame stackFrame = threadReference.frame(0);
LocalVariable localVariable = stackFrame
.visibleVariableByName("str");
Value value = stackFrame.getValue(localVariable);
String str = ((StringReference) value).value();
System.out.println("The local variable str at line 10 is " + str
+ " of " + value.type().name());
eventSet.resume();
} else if (event instanceof VMDisconnectEvent) {
vmExit = true;
} else {
eventSet.resume();
}
}
}
3、下载 jdi.jar 包,然后导入到工程中
4、运行测试,步骤如下
1)、cmd 切换到项目的根目录下,如下图:
2)、编译文件
cmd 执行 javac -g -cp "D:\Program Files\Java\jdk1.8.0_181\lib\tools.jar" com\demo\debug\*.java 命令,如下图:
3)、运行文件
cmd 执行 java -cp ".;D:\Program Files\Java\jdk1.8.0_181\lib\tools.jar" com.demo.debug.SimpleDebugger 命令,运行结果如下图:
调试服务器VM调试处理机制
这里有个问题需要思考一下,当debug 时,VM 是如何处理是否有断点的呢?基本上有两种猜想:一是 VM 执行代码的时候主动检查这行代码是否有断点需要处理,二是客户端动态修改了编译文件的字节码,在需要断点的地方加上了标识
对于第一种猜想,需要看 JVM 的 C 语言源码,目前这个先搁置放一下,对于第二种猜想比较好验证,只需要动态dump出类的class文件即可
dump 出 HelloWorld 文件的 class 文件步骤:
1、下载 dumpclass 文件,放到新建的一个目录下,如图:
2、cmd 中使用 jps 命令查看应用进程id号,如图:
3、cmd 切换到此目录下执行 java -cp "$JAVA_HOME\lib\sa-jdi.jar" -jar dumpclass.jar -p 1448 *HelloWorld 命令,如下图:
反编译 dump出来的 HelloWorld.class 文件,如下图:
结论:VM 是通过主动的方式检查执行的每行代码是否有断点需要处理
如何远程(remote)调试
使用IDE调试是大家最常用的方式,比如idea、eclipse等,运行时候选择debug模式即可,那么如果使用远程调试怎么做的呢?其实很简单,就是启动项目时加上一些参数而已
一、spring web 项目
小于 tomcat9 版本
tomcat 中 bin/catalina.sh 中增加 CATALINA_OPTS='-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18006',如下图所示:
大于等于 tomcat9 版本
tomcat 中 bin/catalina.sh 中的 JPDA_ADDRESS="localhost:8000" 这一句中的localhost修改为0.0.0.0(允许所有ip连接到8000端口,而不仅是本地)8000是端口,端口号可以任意修改成没有占用的即可,如下图所示:
修改之后使用 sh catalina.sh jpda start 命令启动tomcat 即可
二、spring boot 项目远程调试
-jar 后面添加这样的参数,实例如下图:
jdk1.5 之前
-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=8000,suspend=n
jdk1.5 之后
-agentlib:jdwp=transport=dt_socket,address=8800,server=y,suspend=n
上面参数配置好之后,使用 IDEA 进行远程调试,如下图:
1、配置好 remote 远程调试
2、启动调试后请求,如下图:
调试参数详解
-Xdebug :启用调试特性
-Xrunjdwp: <sub-options> 在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项
从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug 和 -Xrunjdwp。但如果连接到 V5 以前的 VM,只能选择 -Xdebug 和 -Xrunjdwp。下面简单描述 -Xrunjdwp 子选项。
-Djava.compiler=NONE: 禁止 JIT 编译器的加载
transport : 传输方式,有 socket 和 shared memory 两种,我们通常使用 socket(套接字)传输,但是在 Windows 平台上也可以使用shared memory(共享内存)传输。
server(y/n): VM 是否需要作为调试服务器执行
address: 调试服务器的端口号,客户端用来连接服务器的端口号
suspend(y/n):值是 y 或者 n,若为 y,启动时候自己程序的 VM 将会暂停(挂起),直到客户端进行连接,若为 n,自己程序的 VM 不会挂起
上面的参数具体可以参看 IDEA 中的,如下图:
参考文档如下:
JPDA调试体系:https://www.ibm.com/developerworks/cn/java/j-lo-jpda1/index.html
Java 调试接口(JDI):https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html
dump出类的class文件:https://blog.csdn.net/hengyunabc/article/details/51106980