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

原理

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

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

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

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值