Java 调试技术 JPDA 架构解读
JPDA 概览
JPDA 的全称是 Java Platform Debugger Architecture,它是 Java 官方针对 Java 代码调试所设计的一个机制。在 Oracle 官网上有专门的页面介绍。它属于多层架构,包括:JVMTI 接口规范、JDWP 通信规范、JDI API 层。
文章图片
Debug 应该是每个程序员都经历过的,日常基于 IDE 的开发中,我们可以给某行代码打上断点,然后以 Debug 模式运行程序,程序运行后会在断点处暂停,这时开发者可以安逸地查看此时各个变量的值,也可以在此时加上更多的断点。
- JVMTI
Java VM Tool Interface 定义的是一系列跟调试相关的接口,由 VM 实现这些接口。我们所说的程序暂停下来,确实是 JVM 运行代码的时候停在了有断点的地方,那么 JVM 必然提供一种联络方式,让别人告诉它断点在哪个类的第几行。这里的联络方式,就是 JVMTI(JVM Tool Interface),这是 JVM 提供的一个类似钩子的机制,通过 JVMTI,可以指挥 JVM 执行某些操作,例如停在断点处,也可以在 JVM 运行过程中,发生某些事件时,通过钩子通知外部感兴趣的人。
那么谁可以跟钩子通信呢,并不是说任何人只要有兴趣就行的。JVM 要求它必须是一个 JVMTI Agent,Java 在不同的操作系统中,都已经内置了本地 JVMTI Agent。在 Windows 系统中,这个 JVMTI Agent 就是一个 DLL 文件,在类 Unix 的操作系统中,则是一个 SO 文件。JVMTI Agent 与 JVM 是运行在同一个机器上同一个进程内的。
- JDWP
当想要利用 JVMTI 让 JVM 做一些事的时候,那么就要先要与 Agent 通信,由它代为传话。因此,JVMTI Agent 内置了一个称之为“通信后端”的模块,用来接收外部的请求。
想要与 JVMTI Agent 通信的第三方,就要先与通信后端通信,通信就意味着必然有一个通信协议的存在,这个协议就是 JDWP(Java Debug Wire Protocol) 协议。
- JDI
Java 程序员经常使用的 eclipse、idea 这样的 IDE 来 debug 程序的时候,就是以 JDWP 协议与目标 JVM 的 JVMTI Agent 通信的。考虑到 JDWP 协议的实现比较繁琐,Java 官方也在 com.sun.jdi 这个 package 中实现了一个叫做 JDI(Java Debug Interface)基础库,JDI 实现了 JDWP 协议,将与 JVMTI Agent 通信的细节封装为一个又一个 Java API,方便第三方与 JVMTI Agent 通信,与 JVMTI Agent 的通信后端相对应,JDI 包含了一个通信前端模块,负责 JDWP 协议的转换以及消息的发送和接收。
JPDA 抽象机制设计为三层:
- 第一层:调试方,由 JDI 定义调试方的 API
- 第二层:通信层,由 JDWP 定义通信协议规范
- 第三层:被调试方,JVMTI 定义如何与目标 JVM 交互
文章图片
为什么 JPDA 机制需要设计层三层呢?原因有几个: - 调试方可能在远程进行调试,而 JDWP 协议又是一个非常底层的二进制协议,实现起来需要花费大量的成本。所以通过 JDI 对 JDWP 协议进行实现,并对外提供 API。
- JDI 不仅仅是实现了 JDWP 协议那么简单,它还实现了队列、缓存、连接初始化等等服务,这些服务都可以简单地通过 JDI 的 API 来使用。
- 有了 JVMTI,调试就可以与具体的 JVM 解耦,不同类型的 JVM 只要遵循 JVMTI 规范即可,JDWP 不需要假设它正在与某种类型的 JVM 通信。
我们可以不通过 JDI,直接自己实现 JDWP 协议与 JVMTI 通信吗,当然可以。
我们可以不通过 JDWP 协议,直接在 JVM 进程中,编写本地 C/C++ 代码与 JVMTI Agent,或者自己实现一个 JVMTI Agent 与目标 JVM 通信吗?当然也是可以的。
这都要从需求出发:
- 如果我们只需要实现一个调试器,例如 IDE,那么我们直接使用 JDI 即可。
- 如果我们的调试器不是用 Java 语言写的,那么,我们需要自行实现 JDWP 协议。
- 如果 JDI/JDWP 所包含的功能不满足我们的需求,例如堆栈分析,那么我们可以直接通过 JVMTI 来实现我们想要的功能。
通信机制
调试器与被调试JVM之间需要通过一定的方式进行通信,通信的机制主要包括两部分
- 连接器(Connector)
- 通信方式(Transport)
通信方式是指调试器与被调试 JVM 之间的数据交换方式和通信报文格式,JPDA 在 JDWP 中定义了报文规范。
连接器 连接器有三种:
- Listening:调试器监听来自被调试 JVM 的连接;
- Attaching:调试器连接上一个已经处于运行状态的被调试 JVM;
- Launching:调试器直接亲手启动被调试 JVM,此时调试器与被调试代码实际上是运行在同一个 JVM 中的;
- 基于 Socket 网络连接,主要用于远程调试,即调试器和被调试 JVM 不在同一台机器上;
文章图片
- 基于操作系统共享内存的通信,主要用于调试器和被调试 JVM 在同一台机器上的情况;
文章图片
调试器和被调试 JVM 在启动的时候, 都需要通过设置 JVM 参数来让它具有调试的能力或者可被调试的能力。
对于 JDK5 及以上的版本,参数格式为:
-agentlib:jdwp={子配置项}
对于 JDK5 以前的版本,参数格式为:
-Xdebug
以及 -Xrunjdwp:{子配置项}
。而子配置项,包括:
- transport:数据交换方式,可选:
dt_socket
和dt_shmem
,分别代表 socket 网络通信和共享内存通信 - Address:标识一个对端的地址,格式为:
{ip}:{port}
- server:标识自己是调试者还是被调试者,调试者配置为:
n
,被调试着配置为:y
- suspend:只有被调试者才需要配这个参数,当配置为
y
的时候,代表等待调试者连接上来才真正启动 Java 应用;配置为n
时,则直接启动 Java 应用。
这里的 Java 应用,是相对于 JVM 来说的,假如把 JVM 看成一个平台,那我们写的代码就是一个 Java 应用。JVM 已经启动,但我们的应用代码还没有跑起来,这种情况在上文的语境中,我们叫做 Java 应用还没启动。配置示例:
- 被调试者开启远程调试监听:
-agentlib:jdwp=transport=dt_socket,address=localhost:7007,server=y,suspend=y
- 被调试者开启本地共享内存调试监听:
-agentlib:jdwp=transport=dt_shmem,server=y,suspend=n
- 调试者远程连接被调试者:
-agentlib:jdwp=transport=dt_socket,address=localhost:7007,server=n,suspend=y
- 调试者基于共享内存方式连接被调试者:
-agentlib:jdwp=transport=dt_shmem, address=
- 调试者基于共享内存方式启动被调试者:
-agentlib:jdwp=transport=dt_shmem,server=y,onuncaught=y,launch=d:\bin\debugstub.exe
被调试者基于共享内存的监听启动后,共享内存地址将会打印到控制台上。调试者配置时需要配置这个共享内存的地址JDI 功能
- 提供了跟调试相关的 Java API;
- 能够获取一个正在运行的 JVM 的状态,包括:类,数组,接口,基本类型以及这些类型的对象数量;
- 与执行相关的控制,例如暂停和恢复线程;
- 设置断点,监听异常的发生、类加载、线程创建等;
- 提供不同的连接器实现,例如基于 socket 的远程连接器和基于共享内存的本地连接器;
文章图片
- 提供事件机制
- 对 JDWP 协议的编解码
要使用 JDI 的功能,需要依赖 JDK 自带的
tools.jar
这个工具包,JDI 相关的代码处于 com.sun.jdi
这个包下面。一个大致的使用步骤如下所示:
- 获取一个
VirtualMachine
实例 - 从
VirtualMachine
实例中获取一个Connector
- 使用
VirtualMachine
的EventRequestManager
来监听我们感兴趣的事件
EventRequestManager em=vm.eventRequestManager();
MethodEntryRequest meR=em.createMethodEntryRequest();
meR.addClassFilter("mypckg.*");
meR.enable();
EventQueue eventQ=vm.eventQueue();
while (running) {
EventSet eventSet=null;
eventSet=eventQ.remove();
EventIterator eventIterator=eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event=eventIterator.nextEvent();
if (event instanceof MethodEntryEvent) {
// process this event
}
vm.resume();
}
}
JDWP 报文格式
请求报文:
文章图片
响应报文:
文章图片
命令集
命名集 | 命令 |
---|---|
Virtual Machine | Version, ClassesBySignature, Suspend, Resume etc |
Reference Type | Signature, ClassLoader, Fields, Methods etc |
Class Type | Super Class, Set Values, Invoke Method, NewInstance |
Array Type | New Instance |
Interface Type | |
Method | Line Table, Variable Table, Byte Codes, IsObsolete etc |
Field | |
Object Reference | Reference Type, Get Values, Set Values, Monitor Info etc |
String Reference | Value |
Thread Reference | Name, Suspend, Resume, Status, Thread Group, Frames etc |
Thread Group Reference | Name, Parent, Childern |
Etc |
接口定义
JVMTI 定义了 JVM 必须实现的一系列用于调试的接口,这些接口总体上包含:
- 获取信息类的接口,例如获取当前堆内存的使用率
- 某种动作,例如设置断点
- 通知,例如当一个断点命中时,通知监听者
- Agent 可以用任何具有调用 C 语言或 C++ 语言能力的语言写,例如 Java。
- 函数、事件、数据类型、常量定义等定义在了基础库 jvmti.h
- Agent 跟目标 JVM 是运行在同一个进程的
- 允许多个 Agent 并行运行,每个 Agent 相互独立
- JDK 本身已经自带一个调试 Agent,在 windows 下以 JDWP.dll 形式存在,在 linux 下以 JDWP.so 形式存在
文章图片
JVM 启动的时候,会调用各个 Agent 的启动函数,如果 Agent 启动了,
Agent_OnLoad
回调函数会被调起。如果 Agent 是中途才 attach 进 JVM 的,那么回调函数是 Agent_OnAttach
。当 Agent 将要被关闭的时候,回调函数
Agent_OnUnload
会被调起。【Java 调试技术 JPDA 架构解读】通过配置 JVM 参数的方式,让 JVM 加载 Agent
- -agentlib:{agent-lib-name}={其它配置项}。例如,配置为:
-agentlib:myagent
,在 windows 平台上,将会搜索 PATH 下的 myagent.dll 文件,在类 Unix 平台上,将会搜索 LD_LIBRARY_PATH 下的 myagent.so 文件。 - -agentpath:{path-to-agent}={其它配置项}。这个配置方式用来配置 Agent 的绝对路径,例如:
-agentpath:d:\myagent\MyAgent.dll
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 事件代理
- Java|Java OpenCV图像处理之SIFT角点检测详解
- java中如何实现重建二叉树
- 移动端h5调试方法
- 数组常用方法一
- 【Hadoop踩雷】Mac下安装Hadoop3以及Java版本问题
- Java|Java基础——数组
- RxJava|RxJava 在Android项目中的使用(一)
- java之static、static|java之static、static final、final的区别与应用