轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

      • Java8--Lambda 表达式、Stream 和时间 API
      • Java集合
      • Java IO
      • Java 文件操作
      • Java 网络编程
      • Java运行期动态能力
      • Java可插入注解处理器
      • Java基准测试(JMH)
      • Java性能分析(Profiler)
      • Java调试(JDI与JDWP)
        • 引言
          • 1 调试Java程序的重要性
          • 2 Java调试的技术背景
        • Java调试接口(JDI)
          • 1 JDI概述
          • 2 JDI的主要组件
          • 虚拟机连接器
          • 虚拟机接口
          • 事件请求和事件处理
          • 3 JDI的应用场景
        • 三、Java调试线协议(JDWP)
          • 1 JDWP概述
          • 2 JDWP的主要组件
          • 命令包
          • 响应包
          • 事件包
          • 3 JDWP的应用场景
        • 四、JDI与JDWP的关系
        • 五、示例
          • 1 使用JDI创建一个简单的调试器
          • 2 使用JDWP实现一个简单的调试器
          • 3 IDEA中的调试
        • 六、结论
      • Java管理与监控(JMX)
      • Java加密体系(JCA)
      • Java服务发现(SPI)
      • Java随机数生成研究
      • Java数据库连接(JDBC)
      • Java历代版本新特性
      • 写好Java Doc
      • 聊聊classpath及其资源获取
    • 并发

    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 核心
轩辕李
2023-05-05
目录

Java调试(JDI与JDWP)

# 引言

# 1 调试Java程序的重要性

调试Java程序是软件开发过程中不可或缺的一环。

通过调试,开发人员可以检查代码是否按照预期运行,找出潜在的问题并解决它们。

有效的调试可以提高代码质量、减少错误和提高开发效率。

# 2 Java调试的技术背景

为了方便Java程序的调试,Java平台提供了一系列调试工具和接口。

本文将重点介绍Java调试接口(JDI)和Java调试线协议(JDWP),它们在Java调试中扮演着关键角色。

# Java调试接口(JDI)

# 1 JDI概述

Java调试接口(Java Debug Interface,JDI)是Java平台调试架构(Java Platform Debugger Architecture,JPDA)的一部分,用于为调试工具提供一个与Java虚拟机(JVM)交互的高级API。JDI允许开发人员创建和控制调试会话,设置断点,检查变量值等。

JPDA包含了JDI和JDWP,它们通过JVMTI和JVM进行通信,实现调试功能。

# 2 JDI的主要组件

# 虚拟机连接器

虚拟机连接器(VirtualMachineConnector)是用于启动和连接到Java虚拟机的组件。开发人员可以使用它来建立与目标虚拟机的连接,从而开始调试会话。

# 虚拟机接口

虚拟机接口(VirtualMachine)是JDI的核心接口,它代表了一个正在运行的Java虚拟机实例。开发人员可以通过这个接口执行各种调试操作,如读取类信息、访问变量值、设置断点等。

# 事件请求和事件处理

事件请求(EventRequest)和事件处理(EventHandler)组件用于在虚拟机中创建和处理事件,如断点触发、线程启动和停止等。开发人员可以根据这些事件执行相应的调试操作。

# 3 JDI的应用场景

JDI广泛应用于各种Java调试工具中,如Eclipse、IntelliJ IDEA等。通过JDI,这些工具可以与Java虚拟机交互,帮助开发人员轻松地调试Java程序。

# 三、Java调试线协议(JDWP)

# 1 JDWP概述

Java调试线协议(Java Debug Wire Protocol,JDWP)是Java平台调试架构(JPDA)的另一个组成部分,用于定义调试器和被调试的Java虚拟机之间的通信协议。JDWP定义了一系列命令和响应,用于实现调试功能。

# 2 JDWP的主要组件

# 命令包

命令包(Command Packet)是用于封装JDWP命令的数据结构。它包含一个命令集ID、命令ID、数据长度和数据负载。调试器可以通过发送命令包来请求执行特定的调试操作。

# 响应包

响应包(Reply Packet)是用于封装JDWP响应的数据结构。它包含一个响应ID、响应代码、数据长度和数据负载。Java虚拟机在处理完命令包后,会通过响应包将结果返回给调试器。

# 事件包

事件包(Event Packet)是用于封装JDWP事件的数据结构。它包含一个事件ID、事件类型、数据长度和数据负载。当某个事件发生时,如断点触发或线程启动,Java虚拟机会通过事件包通知调试器。

# 3 JDWP的应用场景

JDWP主要应用于Java调试器和Java虚拟机之间的通信。通过JDWP,调试器可以发送命令请求并接收响应,从而实现对Java虚拟机的控制和监视。

# 四、JDI与JDWP的关系

JDI和JDWP是Java平台调试架构(JPDA)的两个关键组件。JDI为调试工具提供了一个高级API,用于与Java虚拟机交互。而JDWP定义了调试器和Java虚拟机之间的通信协议。

实际上,JDI的实现通常基于JDWP。也就是说,当调试工具通过JDI执行调试操作时,它实际上是在发送JDWP命令给Java虚拟机。

# 五、示例

# 1 使用JDI创建一个简单的调试器

假设我们有以下简单的Java程序作为我们要调试的应用:

// TestApp.java
public class TestApp {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        int result = add(1, 2);
        System.out.println("The result is: " + result);
    }

    private static int add(int a, int b) {
        return a + b;
    }
}

为了使用SimpleDebugger来调试这个应用,首先编译TestApp.java:

javac TestApp.java

然后,在另一个终端窗口中启动TestApp,并指定调试参数以便SimpleDebugger可以连接到它:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000 TestApp

现在,我们需要修改SimpleDebugger的代码以便在虚拟机启动后附加到我们的TestApp:

import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.event.*;

public class SimpleDebugger {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个虚拟机连接器
        VirtualMachineManager vmMgr = Bootstrap.virtualMachineManager();
        AttachingConnector connector = findConnector(vmManager, "dt_socket");
        Map<String, Connector.Argument> arguments = connector.defaultArguments();
        arguments.get("hostname").setValue("localhost");
        arguments.get("port").setValue("8000");
        VirtualMachine vm = connector.attach(arguments);

        // 2. 设置连接参数
        Map<String, Connector.Argument> connArgs = launchingConnector.defaultArguments();
        connArgs.get("main").setValue("com.example.MyClass");

        // 3. 启动并连接到Java虚拟机
        VirtualMachine vm = launchingConnector.launch(connArgs);

        // 4. 设置一个断点
        ReferenceType refType = vm.classesByName("com.example.MyClass").get(0);
        Location breakpointLocation = refType.locationsOfLine(15).get(0);
        EventRequestManager erm = vm.eventRequestManager();
        BreakpointRequest bpReq = erm.createBreakpointRequest(breakpointLocation);
        bpReq.enable();

        // 5. 监听事件
        EventQueue eventQueue = vm.eventQueue();
        boolean vmExited = false;
        while (!vmExited) {
            EventSet eventSet = eventQueue.remove();
            for (Event event : eventSet) {
                if (event instanceof BreakpointEvent) {
                    // 6. 处理断点事件
                    BreakpointEvent breakpointEvent = (BreakpointEvent) event;
                    ThreadReference thread = breakpointEvent.thread();
                    StackFrame frame = thread.frame(0);
                    System.out.println("Breakpoint hit at: " + breakpointEvent.location());
                    System.out.println("Current frame: " + frame);
                } else if (event instanceof VMDisconnectEvent) {
                    // 7. 处理虚拟机断开连接事件
                    vmExited = true;
                }
            }
            eventSet.resume();
        }
    }

    private static AttachingConnector findConnector(VirtualMachineManager vmManager, String connectorName) {
        for (AttachingConnector connector : vmManager.attachingConnectors()) {
            if (connector.name().equalsIgnoreCase(connectorName)) {
                return connector;
            }
        }
        throw new IllegalStateException("Unable to find connector with name: " + connectorName);
    }

}

接下来,编译SimpleDebugger.java:

javac -classpath <path_to_tools.jar> SimpleDebugger.java

请确保在编译时将tools.jar添加到类路径。tools.jar位于JDK的lib目录中。在较新的JDK版本中(如JDK 9及更高版本),请使用jdk.jdi模块代替。

最后,运行SimpleDebugger:

java -classpath <path_to_tools.jar>:. SimpleDebugger

注意:Linux下使用冒号分割classpath,Windows下请使用分号

当SimpleDebugger成功连接到TestApp并在add方法中设置断点后,你应该会看到类似以下输出:

Breakpoint hit at: TestApp.add(TestApp.java:9)
Current frame: TestApp.add(TestApp.java:9)

这就是如何使用SimpleDebugger来调试一个Java应用程序。

# 2 使用JDWP实现一个简单的调试器

以下是一个使用JDWP实现的简单调试器的完整示例代码。

为了演示方便,我们使用了硬编码的端口号8000。在实际使用中,你需要将其替换为实际调试目标虚拟机监听的端口号。

import java.io.*;
import java.net.Socket;

public class SimpleJDWPDebugger {

    public static void main(String[] args) throws Exception {
        // 1. 连接到Java虚拟机
        Socket socket = new Socket("localhost", 8000);
        DataInputStream in = new DataInputStream(socket.getInputStream());
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());

        // 2. 发送JDWP命令并接收响应
        byte[] commandPacket = createCommandPacket();
        out.write(commandPacket);

        byte[] header = new byte[11];
        in.readFully(header);
        int replyPacketLength = readInt(header, 0) - 11;
        byte[] replyPacket = new byte[replyPacketLength];
        in.readFully(replyPacket);

        // 3. 处理响应
        handleReplyPacket(replyPacket);

        // 4. 关闭连接
        in.close();
        out.close();
        socket.close();
    }

    private static byte[] createCommandPacket() {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);

        try {
            dos.writeInt(11); // 数据包长度
            dos.writeInt(1); // 数据包ID
            dos.writeByte(0); // flags: 0 for command
            dos.writeShort(1); // 命令集ID(VirtualMachine)
            dos.writeShort(3); // 命令ID(AllClasses)
        } catch (IOException e) {
            e.printStackTrace();
        }

        return baos.toByteArray();
    }

    private static void handleReplyPacket(byte[] replyPacket) throws IOException {
        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(replyPacket));

        int numberOfClasses = dis.readInt();
        System.out.println("已加载的类数量: " + numberOfClasses);

        for (int i = 0; i < numberOfClasses; i++) {
            byte refTypeTag = dis.readByte();
            long classId = dis.readLong();
            String signature = readString(dis);
            int status = dis.readInt();

            System.out.println("Class ID: " + classId + ", Signature: " + signature);
        }
    }

    private static int readInt(byte[] bytes, int offset) {
        return ((bytes[offset] & 0xFF) << 24) | ((bytes[offset + 1] & 0xFF) << 16) | ((bytes[offset + 2] & 0xFF) << 8) | (bytes[offset + 3] & 0xFF);
    }

    private static String readString(DataInputStream dis) throws IOException {
        int length = dis.readInt();
        byte[] bytes = new byte[length];
        dis.readFully(bytes);
        return new String(bytes, "UTF-8");
    }
}

要使用这个简单的调试器,你需要以下步骤:

  1. 首先,启动你想调试的Java程序,并使用-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000参数启用JDWP代理。例如:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 -classpath your_classpath your_main_class
  1. 然后,运行SimpleJDWPDebugger。它将连接到步骤1中启动的Java虚拟机,并发送一个JDWP命令包。

  2. 在createCommandPacket方法中,根据需要创建一个适当的JDWP命令包。你可以参考JDWP规范 (opens new window)来了解如何创建JDWP命令包。

  3. 在处理响应部分,你可以根据JDWP规范解析响应包,然后根据需要执行相应的操作,如打印调试信息、设置断点等。

注意:这个示例仅用于演示目的,实际上我们通常会使用成熟的调试库,如JDI,它为我们处理了底层的JDWP通信。

# 3 IDEA中的调试

在IDEA中运行调试,控制台会输出:

...\.jdks\corretto-17.0.3\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59516,suspend=y,server=n -javaagent:C:\Users\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar=file:/C:/Users/AppData/Local/Temp/capture.props ...

这个命令行是在启动一个 Java 程序并使用 IDEA 的调试功能进行调试。具体的解释如下:

  • ...\.jdks\corretto-17.0.3\bin\java.exe:表示使用 Corretto 17 JDK 中的 java 命令来启动程序。
  • -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59516,suspend=y,server=n:表示使用 JDWP 调试协议来进行调试,使用 Socket 传输方式,端口号为 59516,等待调试器连接,暂停程序的执行。
  • -javaagent:C:\Users\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar=file:/C:/Users/AppData/Local/Temp/capture.props:表示使用 IDEA 的调试代理进行调试,并传递一个配置文件的路径。

# 六、结论

JDI和JDWP是Java平台调试架构中的关键组件,它们为Java程序的调试提供了强大的支持。

通过理解JDI和JDWP的原理和功能,开发人员可以更好地利用调试工具,提高开发效率和代码质量。

祝你变得更强!

编辑 (opens new window)
#Java调试#JDI#JDWP
上次更新: 2023/06/14
Java性能分析(Profiler)
Java管理与监控(JMX)

← Java性能分析(Profiler) Java管理与监控(JMX)→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式