(转)Java Debug

ReZero lol

Debug 基础知识笔记 (一)

JDK 1.4.x:
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005

JDK5-8:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

JDK9 or later:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

远程 debug 的机理大致猜一下应该也就是客户端和远程虚拟机建立socket链接,然后通过约定的协议来进行数据的交互进而执行各自的功能逻辑。

从命令上也看的出来一些端倪:1.4 到 8 Xdebug 和 agentlib 转换其实类似个语法糖的转换,无关紧要。
(查了下 hotspot jdk8 source code 大致是在parse_each_vm_init_arg这个函数里进行的解析,看函数名猜下就是 vm 初始化参数)。
然后 debug 挂起模式(这个参数设置为 y 的时候一般是针对无法启动项目的 debug),地址, 端口大意都能理解。那么好奇就在于解析的这个 jdwp 是个啥了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// openjdk\hotspot\src\share\vm\runtime\arguments.cpp 2597 parse_each_vm_init_arg
if (match_option(option, "-agentlib:", &tail) ||
(is_absolute_path = match_option(option, "-agentpath:", &tail))) {
if(tail != NULL) {
... 处理字符串解析路径参数
if(pos != NULL) {
options = strcpy(NEW_C_HEAP_ARRAY(char, strlen(pos + 1) + 1, mtInternal), pos + 1);
}
#if !INCLUDE_JVMTI
if (valid_hprof_or_jdwp_agent(name, is_absolute_path)) {
jio_fprintf(defaultStream::error_stream(),
"Profiling and debugging agents are not supported in this VM\n");
return JNI_ERR;
}
#endif // !INCLUDE_JVMTI
add_init_agent(name, options, is_absolute_path);
}

static void add_init_agent(const char* name, char* options, bool absolute_path)
{ _agentList.add(new AgentLibrary(name, options, absolute_path, NULL)); }

解析出来的agent 会被添加到 agentlist 中去

上述结束后, jvm 在启动时回去判断这个list 是不是空的,不为空,就加载他们并执行 agentOnLoad 函数进而执行真正的 agent 功能逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (Arguments::init_agents_at_startup()) {
create_vm_init_agents();
}

void Threads::create_vm_init_agents() {
AgentLibrary* agent;
for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
OnLoadEntry_t on_load_entry = lookup_agent_on_load(agent);
if (on_load_entry != NULL) {
// 反射调用AgentOnLoad
jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
}
}
}

看下 agentOnLoad的注释,大致是说在动态链接库被加载时会立刻调用【即动态链库加载后执行的第一个方法】

重点类 JPLISAgent(Java Programming Language Instrumentation Services Agent),它的作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任。

在JVM启动的时候,JVM会通过-javaagent参数加载Agent。最开始加载的是libinstrument动态链接库,然后在动态链接库里面找到JVMTI的入口方法:Agent_OnLoad。下面就来分析一下在libinstrument动态链接库中,Agent_OnLoad函数是怎么实现的。

Attach机制的奥秘所在,也就是Attach Listener线程的创建依靠Signal Dispatcher线程,Signal Dispatcher是用来处理信号的线程,当Signal Dispatcher线程接收到“SIGBREAK”信号之后,就会执行初始化Attach Listener的工作。

debug 概览:

JPDA 体系概览

  1. JVMTI 实际调用的方法,相当于 元命令【获取及控制当前虚拟机状态】

JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。

  1. JDWP 类似 Model 模型数据,作为中间数据转换,用来兼容双向接口 【定义 JVMTI 和 JDI 交互的数据格式】

JDWP(Java Debug Wire Protocol)是一个为 Java 调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。在 JPDA 体系中,作为前端(front-end)的调试者(debugger)进程和后端(back-end)的被调试程序(debuggee)进程之间的交互数据的格式就是由 JDWP 来描述的,它详细完整地定义了请求命令、回应数据和错误代码,保证了前端和后端的 JVMTI 和 JDI 的通信通畅。比如在 Sun 公司提供的实现中,它提供了一个名为 jdwp.dll(jdwp.so)的动态链接库文件,这个动态库文件实现了一个 Agent,它会负责解析前端发出的请求或者命令,并将其转化为 JVMTI 调用,然后将 JVMTI 函数的返回值封装成 JDWP 数据发还给后端。

另外,这里需要注意的是 JDWP 本身并不包括传输层的实现,传输层需要独立实现,但是 JDWP 包括了和传输层交互的严格的定义,就是说,JDWP 协议虽然不规定我们是通过 EMS 还是快递运送货物的,但是它规定了我们传送的货物的摆放的方式。在 Sun 公司提供的 JDK 中,在传输层上,它提供了 socket 方式,以及在 Windows 上的 shared memory 方式。当然,传输层本身无非就是本机内进程间通信方式和远端通信方式,用户有兴趣也可以按 JDWP 的标准自己实现。

  1. JDI 相当于 java 层面的封装工具,便于使用 JVMTI 和 格式话 JDWP 数据 【提供 Java API 来远程控制被调试虚拟机】

JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 JDI 由针对前端定义的接口组成,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行,JDI 不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务。从理论上说,开发人员只需使用 JDWP 和 JVMTI 即可支持跨平台的远程调试,但是直接编写 JDWP 程序费时费力,而且效率不高。因此基于 Java 的 JDI 层的引入,简化了操作,提高了开发人员开发调试程序的效率。

以 C/C++ 的调试为例,它们 Debugger 本身事实上是提供了,或者说,创建和管理了一个运行态,因此他们的程序算法比较复杂,个头都比较大。

而 Java 的可控的运行态明显就是虚拟机。 Java 的 JPDA 就是一套为调试和优化服务的虚拟机的操作工具,其中,JVMTI 是整合在虚拟机中的接口,JDWP 是一个通讯层,而 JDI 是前端为开发人员准备好的工具和运行库。

从构架上说,我们可以把 JPDA 看作成是一个 C/S 体系结构的应用,在这个构架下,我们可以方便地通过网络,在任意的地点调试另外一个虚拟机上的程序,这个就很好地解决了部署和测试的问题,尤其满足解决了很多网络时代中的开发应用的需求。前端和后端的分离,也方便用户开发适合于自己的调试工具。

从效率上看,由于 Java 程序本身就是编译成字节码,运行在虚拟机上的,因此调试前后的程序、内存占用都不会有大变化(仅仅是启动一个 JDWP 所需要的内存),任意程度都可以很好地调试,非常方便。而 JPDA 构架下的几个组成部分,JDWP 和 JDI 都比较小,主要的工作可以让虚拟机自己完成。

从灵活性上,Java 调试工具是建立在强大的虚拟机上的,因此,很多前沿的应用,比如动态编译运行,字节码的实时替换等等,都可以通过对虚拟机的改进而得到实现。随着虚拟机技术的逐步发展和深入,各种不同种类,不同应用领域中虚拟机的出现,各种强大的功能的加入,给我们的调试工具也带来很多新的应用。

总而言之,一个先天的,可控的运行态给 Java 的调试工作,给 Java 调试接口带来了极大的优势和便利。通过 JPDA 这个标准,我们可以从虚拟机中得到我们所需要的信息,完成我们所希望的操作,更好地开发我们的程序。

JVMTI 和 Agent 实现

在 Java 程序运行的过程中,程序员希望掌握它总体的运行状况,这个时候程序员可以直接使用 JDK 提供的 jconsole 程序。如果希望提高程序的执行效率,开发人员可以使用各种 Java Profiler。OutOfMemoryError)等等,这时可以把当前的内存输出到 Dump 文件,再使用堆分析器或者 Dump 文件分析器等工具进行研究,查看当前运行态堆(Heap)中存在的实例整体状况来诊断问题。

上述情况的共同点,与虚拟机交互来获取信息

JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。

Agent Working process

Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期

可以做的事: 操作 JVMTI 的 Capability 参数【就是事件回调绑定的说明,通过这个参数来说明发生事件时对应的回调,其实也就是JVMTI的主要工作方式】; 使用系统参数;前面提到动态库加载后会调用 Agent_OnLoad(JavaVM *vm, char *options, void *reserved),传入的第一个虚拟机参数就可以拿到需要的VM,进而获取到对应的 JVMTI 来使用。第二个参数就是命令行参数:该参数在结束ONLoad后会被清空。第三个是版本信息。
tips: 版本信息参数会影响到不同的虚拟机实现。另外此时并非所有的JVMTI函数都可用,部分受限于尚未完成的初始化工作(其实JVMTI 的函数调用都有其时间性,即特定的函数只能在特定的虚拟机状态下才能调用,比如 SuspendThread(挂起线程)这个动作,仅在 Java 虚拟机处于运行状态(live phase)才能调用,否则导致一个内部异常。)。

标准的 jvmtiCapabilities 定义了一系列虚拟机的功能,比如 can_redefine_any_class 定义了虚拟机是否支持重定义类,can_retransform_classes 定义了是否支持在运行的时候改变类定义等等。

1
2
3
4
err = (*jvmti)->GetCapabilities(jvmti, &capa); // 取得 jvmtiCapabilities 指针。
if (err == JVMTI_ERROR_NONE) {
if (capa.can_redefine_any_class) { ... }
} // 查看是否支持重定义类

jVMTI 的工作模式就是根据已有定义的事件配置回调来完成触发动作实现

1
2
3
4
5
jvmtiEventCallbacks eventCallBacks; 
memset(&ecbs, 0, sizeof(ecbs)); // 初始化
eventCallBacks.ThreadStart = &HandleThreadStart; // 设置函数指针
// 上面 写定了一个 线程启动事件绑定的回调函数,然后将其注册到设置中即可,如下
jvmti->SetEventCallbacks(eventCallBacks, sizeof(eventCallBacks));

JDWP 及实现

JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。

Debugger 通过发送 command packet 获取 target Java 虚拟机的信息以及控制程序的执行。Target Java 虚拟机通过发送 command packet 通知 debugger 某些事件的发生,如到达断点或是产生异常。

Reply packet 是用来回复 command packet 该命令是否执行成功,如果成功 reply packet 还有可能包含 command packet 请求的数据,比如当前的线程信息或者变量的值。从 target Java 虚拟机发送的事件消息是不需要回复的。

还有一点需要注意的是,JDWP 是异步的:command packet 的发送方不需要等待接收到 reply packet 就可以继续发送下一个 command packet。

  • Post title:(转)Java Debug
  • Post author:ReZero
  • Create time:2020-07-04 11:10:00
  • Post link:https://rezeros.github.io/2020/07/04/Java-Debug/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments