分享

深入 java debug 原理及远程remote调试详解

 一本正经地胡闹 2021-11-17

原理

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、新建调试程序代码

  1. package com.demo.debug;
  2. public class HelloWorld {
  3. public static void main(String[] args) {
  4. String str = "Hello world!";
  5. System.out.println(str);
  6. }
  7. }

2、调试程序客户端代码

  1. package com.demo.debug;
  2. import com.sun.jdi.Bootstrap;
  3. import com.sun.jdi.LocalVariable;
  4. import com.sun.jdi.Location;
  5. import com.sun.jdi.ReferenceType;
  6. import com.sun.jdi.StackFrame;
  7. import com.sun.jdi.StringReference;
  8. import com.sun.jdi.ThreadReference;
  9. import com.sun.jdi.Value;
  10. import com.sun.jdi.VirtualMachine;
  11. import com.sun.jdi.connect.Connector;
  12. import com.sun.jdi.connect.LaunchingConnector;
  13. import com.sun.jdi.event.BreakpointEvent;
  14. import com.sun.jdi.event.ClassPrepareEvent;
  15. import com.sun.jdi.event.Event;
  16. import com.sun.jdi.event.EventIterator;
  17. import com.sun.jdi.event.EventQueue;
  18. import com.sun.jdi.event.EventSet;
  19. import com.sun.jdi.event.VMDisconnectEvent;
  20. import com.sun.jdi.event.VMStartEvent;
  21. import com.sun.jdi.request.BreakpointRequest;
  22. import com.sun.jdi.request.ClassPrepareRequest;
  23. import com.sun.jdi.request.EventRequest;
  24. import com.sun.jdi.request.EventRequestManager;
  25. import java.util.List;
  26. import java.util.Map;
  27. public class SimpleDebugger {
  28. static VirtualMachine vm;
  29. static Process process;
  30. static EventRequestManager eventRequestManager;
  31. static EventQueue eventQueue;
  32. static EventSet eventSet;
  33. static boolean vmExit = false;
  34. public static void main(String[] args) throws Exception {
  35. LaunchingConnector launchingConnector
  36. = Bootstrap.virtualMachineManager().defaultConnector();
  37. // Get arguments of the launching connector
  38. Map<String, Connector.Argument> defaultArguments
  39. = launchingConnector.defaultArguments();
  40. Connector.Argument mainArg = defaultArguments.get("main");
  41. Connector.Argument suspendArg = defaultArguments.get("suspend");
  42. // Set class of main method
  43. mainArg.setValue("com.demo.debug.HelloWorld");
  44. suspendArg.setValue("true");
  45. vm = launchingConnector.launch(defaultArguments);
  46. process = vm.process();
  47. // Register ClassPrepareRequest
  48. eventRequestManager = vm.eventRequestManager();
  49. ClassPrepareRequest classPrepareRequest
  50. = eventRequestManager.createClassPrepareRequest();
  51. classPrepareRequest.addClassFilter("com.demo.debug.HelloWorld");
  52. classPrepareRequest.addCountFilter(1);
  53. classPrepareRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
  54. classPrepareRequest.enable();
  55. // Enter event loop
  56. eventLoop();
  57. process.destroy();
  58. }
  59. private static void eventLoop() throws Exception {
  60. eventQueue = vm.eventQueue();
  61. while (true) {
  62. if (vmExit == true) {
  63. break;
  64. }
  65. eventSet = eventQueue.remove();
  66. EventIterator eventIterator = eventSet.eventIterator();
  67. while (eventIterator.hasNext()) {
  68. Event event = (Event) eventIterator.next();
  69. execute(event);
  70. }
  71. }
  72. }
  73. private static void execute(Event event) throws Exception {
  74. if (event instanceof VMStartEvent) {
  75. System.out.println("VM started");
  76. eventSet.resume();
  77. } else if (event instanceof ClassPrepareEvent) {
  78. ClassPrepareEvent classPrepareEvent = (ClassPrepareEvent) event;
  79. String mainClassName = classPrepareEvent.referenceType().name();
  80. if (mainClassName.equals("com.demo.debug.HelloWorld")) {
  81. System.out.println("Class " + mainClassName
  82. + " is already prepared");
  83. }
  84. if (true) {
  85. // Get location
  86. ReferenceType referenceType = classPrepareEvent.referenceType();
  87. List locations = referenceType.locationsOfLine(10);
  88. Location location = (Location) locations.get(0);
  89. // Create BreakpointEvent
  90. BreakpointRequest breakpointRequest = eventRequestManager
  91. .createBreakpointRequest(location);
  92. breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
  93. breakpointRequest.enable();
  94. }
  95. eventSet.resume();
  96. } else if (event instanceof BreakpointEvent) {
  97. System.out.println("Reach line 10 of com.demo.debug.HelloWorld");
  98. BreakpointEvent breakpointEvent = (BreakpointEvent) event;
  99. ThreadReference threadReference = breakpointEvent.thread();
  100. StackFrame stackFrame = threadReference.frame(0);
  101. LocalVariable localVariable = stackFrame
  102. .visibleVariableByName("str");
  103. Value value = stackFrame.getValue(localVariable);
  104. String str = ((StringReference) value).value();
  105. System.out.println("The local variable str at line 10 is " + str
  106. + " of " + value.type().name());
  107. eventSet.resume();
  108. } else if (event instanceof VMDisconnectEvent) {
  109. vmExit = true;
  110. } else {
  111. eventSet.resume();
  112. }
  113. }
  114. }

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

示例demo:http:///java-debug-interface-api-jdi-hello-world-example-programmatic-debugging-for-beginners/

dump出类的class文件:https://blog.csdn.net/hengyunabc/article/details/51106980

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多