




已阅读5页,还剩40页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
深入Java调试体系: 第1部分,JPDA体系概览JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI),本系列将会详细介绍这三个模块的内部细节、通过实例为读者揭开 JPDA 的面纱。本文是该系列的第一篇,将会着重从整体上介绍 JPDA 的各个组成,阐述它们彼此之间的内在关联。JPDA 概述所有的程序员都会遇到 bug,对于运行态的错误,我们往往需要一些方法来观察和测试运行态中的环境。在 Java 程序中,最简单的,您是否尝试过使用 System.out.println() 来输出您的 Java 程序的执行中的各种变量状态来发现您的 Java 程序运行时的问题?这种方式方便易用,在一些简单的情况下能够解决您的问题,但是如果当您的程序运行在远程环境上,或者当前环境不允许控制台终端输出(比 如,考虑一下虚拟机初始化之时),您无法获取终端输出的时候呢?或者,如果您根本无法本地修改运行您的程序?无须担心,您可以通过很多的调试工具来帮助您解决这个问题,常见的 IDE 都附带一个非常直观简单的调试工具,比如 Eclipse(图 1)就提供一个功能非常全面,操作非常简单的调试器。图 1. 使用 Eclipse 调试 Java 程序其他的一些常见的 Java IDE,比如 Netbeans 和 IntelliJ 等等也都提供了类似的功能,您甚至能不用 IDE 提供的图形界面,使用 JDK 自带的 jdb 工具,以文本命令的形式来调试您的 Java 程序。这些形形色色的调试器都支持本地和远程的程序调试,那么它们是如何被开发的?它们之间存在着什么样的联系呢?我们不得不提及 Java 的调试体系 JPDA 。我们知道,Java 程序都是运行在 Java 虚拟机上的,我们要调试 Java 程序,事实上就需要向 Java 虚拟机请求当前运行态的状态,并对虚拟机发出一定的指令,设置一些回调等等,那么 Java 的调试体系,就是虚拟机的一整套用于调试的工具和接口。对于 Java 虚拟机接口熟悉的人来说,您一定还记得 Java 提供了两个接口体系,JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface),而它们,以及在 Java SE 5 中准备代替它们的 JVMTI(Java Virtual Machine Tool Interface),都是 Java 平台调试体系(Java Platform Debugger Architecture,JPDA)的重要组成部分。 Java SE 自 1.2.2 版就开始推出 Java 平台调试体系结构(JPDA)工具集,而从 JDK 1.3.x 开始,Java SDK 就提供了对 Java 平台调试体系结构的直接支持。顾名思义,这个体系为开发人员提供了一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议。本质上说,它是我们通向虚拟机,考察虚拟机运行态的一个通道,一套工具。理解这一点对于学习 JPDA 非常重要。换句话说,通过 JPDA 这套接口,我们就可以开发自己的调试工具。通过这些 JPDA 提供的接口和协议,调试器开发人员就能根据特定开发者的需求,扩展定制 Java 调试应用程序,开发出吸引开发人员使用的调试工具。前面我们提到的 IDE 调试工具都是基于 JPDA 体系开发的,区别仅仅在于它们可能提供了不同的图形界面、具有一些不同的自定义功能。另外,我们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准,因此,通过 JPDA 开发出来的调试工具先天具有跨平台、不依赖虚拟机实现、JDK 版本无关等移植优点,因此大部分的调试工具都是基于这个体系的。JPDA 组成模块JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI)。这三个模块把调试过程分解成几个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。 被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试 结果。在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。图 2 展示了这个过程:图 2. JPDA 模块层次当然,开发人员完全可以不使用完整的三个层次,而是基于其中的某一个层次开发自己的应用。比如您完全可以仅仅依靠通过 JVMTI 函数开发一个调试工具,而不使用 JDWP 和 JDI,只使用自己的通讯和命令接口。当然,除非是有特殊的需求,利用已有的实现会使您事半功倍,避免重复发明轮子。这三个模块我们会在后续文章中分别详细介绍,这里我们简单介绍它们的主要功能:Java 虚拟机工具接口(JVMTI)JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,它处于整个 JPDA 体系的最底层,所有调试功能本质上都需要通过 JVMTI 来提供。通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。我们知道,JVMTI 的前身是 JVMDI 和 JVMPI,它们原来分别被用于提供调试 Java 程序以及 Java 程序调节性能的功能。在 J2SE 5.0 之后 JDK 取代了 JVMDI 和 JVMPI 这两套接口,JVMDI 在最新的 Java SE 6 中已经不提供支持,而 JVMPI 也计划在 Java SE 7 后被彻底取代。Java 调试线协议(JDWP)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 的标准自己实现。Java 调试接口(JDI)JDI(Java Debug Interface)是三个模块中最高层的接口,在多数的 JDK 中,它是由 Java 语言实现的。 JDI 由针对前端定义的接口组成,通过它,调试工具开发人员就能通过前端虚拟机上的调试器来远程操控后端虚拟机上被调试程序的运行,JDI 不仅能帮助开发人员格式化 JDWP 数据,而且还能为 JDWP 数据传输提供队列、缓存等优化服务。从理论上说,开发人员只需使用 JDWP 和 JVMTI 即可支持跨平台的远程调试,但是直接编写 JDWP 程序费时费力,而且效率不高。因此基于 Java 的 JDI 层的引入,简化了操作,提高了开发人员开发调试程序的效率。表 1 总结了三个模块的不同点:表 1. JPDA 层次比较模块层次编程语言作用JVMTI底层C获取及控制当前虚拟机状态JDWP中介层C定义 JVMTI 和 JDI 交互的数据格式JDI高层Java提供 Java API 来远程控制被调试虚拟机JPDA 实现每一个虚拟机都应该实现 JVMTI 接口,但是 JDWP 和 JDI 本身与虚拟机并非是不可分的,这三个层之间是通过标准所定义的交互的接口和协议联系起来的,因此它们可以被独立替换或取代,但不会影响到整体调试工具的开 发和使用。因此,开发和使用自己的 JDWP 和 JDI 接口实现是可能的。Java 软件开发包(SDK)标准版里提供了 JPDA 三个层次的标准实现,事实上,调试工具开发人员还有很多其他开源实现可以选择,比如 Apache Harmony 提供了 JDWP 的实现。而 JDI,我们可以在 Eclipse 一个子项目 org.eclipse.jdt.debug 里找到其完整的实现(Harmony 也使用了这套实现,作为其 J2SE 类库的一部分)。通过标准协议,Eclipse IDE 的调试工具就可以完全在 Harmony 的环境上运行。Java 调试接口的特点Java 语言是第一个使用虚拟机概念的流行的编程语言,正是因为虚拟机的存在,使很多事情变得简单而轻松,掌握了虚拟机,就掌握了内存分配、线程管理、即时优化等 等运行态。同样的,Java 调试的本质,就是和虚拟机打交道,通过操作虚拟机来达到观察调试我们自己代码的目的。这个特点决定了 Java 调试接口和以前其他编程语言的巨大区别。以 C/C+ 的调试为例,目前比较流行的调试工具是 GDB 和微软的 Visual Studio 自带的 debugger,在这种 debugger 中,首先,我们必须编译一个“ debug ”模式的程序,这个会比实际的 release 模式程序大很多。其次,在调试过程中,debugger 将会深层接入程序的运行,掌握和控制运行态的一些信息,并将这些信息及时返回。这种介入对运行的效率和内存占用都有一定的需求。基于这些需求,这些 Debugger 本身事实上是提供了,或者说,创建和管理了一个运行态,因此他们的程序算法比较复杂,个头都比较大。对于远端的调试,GDB 也没有很好的默认实现,当然,C/C+ 在这方面也没有特别大的需求。而 Java 则不同,由于 Java 的运行态已经被虚拟机所很好地管理,因此作为 Java 的 Debugger 无需再自己创造一个可控的运行态,而仅仅需要去操作虚拟机就可以了。 Java 的 JPDA 就是一套为调试和优化服务的虚拟机的操作工具,其中,JVMTI 是整合在虚拟机中的接口,JDWP 是一个通讯层,而 JDI 是前端为开发人员准备好的工具和运行库。从构架上说,我们可以把 JPDA 看作成是一个 C/S 体系结构的应用,在这个构架下,我们可以方便地通过网络,在任意的地点调试另外一个虚拟机上的程序,这个就很好地解决了部署和测试的问题,尤其满足解决了 很多网络时代中的开发应用的需求。前端和后端的分离,也方便用户开发适合于自己的调试工具。从效率上看,由于 Java 程序本身就是编译成字节码,运行在虚拟机上的,因此调试前后的程序、内存占用都不会有大变化(仅仅是启动一个 JDWP 所需要的内存),任意程度都可以很好地调试,非常方便。而 JPDA 构架下的几个组成部分,JDWP 和 JDI 都比较小,主要的工作可以让虚拟机自己完成。从灵活性上,Java 调试工具是建立在强大的虚拟机上的,因此,很多前沿的应用,比如动态编译运行,字节码的实时替换等等,都可以通过对虚拟机的改进而得到实现。随着虚拟机技 术的逐步发展和深入,各种不同种类,不同应用领域中虚拟机的出现,各种强大的功能的加入,给我们的调试工具也带来很多新的应用。总而言之,一个先天的,可控的运行态给 Java 的调试工作,给 Java 调试接口带来了极大的优势和便利。通过 JPDA 这个标准,我们可以从虚拟机中得到我们所需要的信息,完成我们所希望的操作,更好地开发我们的程序。结束语本文简单介绍了 JPDA 的三个模块以及它们如何和其它层次交互,让读者在整体上对 JPDA 体系有了一个直观的了解,从而方便后面针对每个模块具体介绍的学习,这里我们学习到:JPDA 定义了一套如何开发调试工具的接口和规范。JPDA 由三个独立的模块 JVMTI、JDWP、JDI 组成。调试者通过 JDI 发送接受调试命令。JDWP 定义调试者和被调试者交流数据的格式。JVMTI 可以控制当前虚拟机运行状态。除了标准实现,JPDA 还有许多开源实现供使用。Java 调试工具的优点。深入Java调试体系,第2部分: JVMTI和Agent实现JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写。通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI)、Java 调试线协议(JDWP),以及 Java 调试接口(JDI)。本系列将会详细介绍这三个模块的内部细节,并通过实例为读者揭开 JPDA 的面纱。本系列的 第 1 部分 从整体上介绍 JPDA 的各个组成,以及它们彼此之间的内在关联。本文是该系列的第 2 篇,将会着重介绍强大的虚拟机接口 - JVMTI,以及如何使用 JVMTI 编写用户自定义的 Java 调试和诊断程序。Java 程序的诊断和调试开发人员对 Java 程序的诊断和调试有许多不同种类、不同层次的需求,这就使得开发人员需要使用不同的工具来解决问题。比如,在 Java 程序运行的过程中,程序员希望掌握它总体的运行状况,这个时候程序员可以直接使用 JDK 提供的 jconsole 程序。如果希望提高程序的执行效率,开发人员可以使用各种 Java Profiler。这种类型的工具非常多,各有优点,能够帮助开发人员找到程序的瓶颈,从而提高程序的运行速度。开发人员还会遇到一些与内存相关的问题, 比如内存占用过多,大量内存不能得到释放,甚至导致内存溢出错误(OutOfMemoryError)等等,这时可以把当前的内存输出到 Dump 文件,再使用堆分析器或者 Dump 文件分析器等工具进行研究,查看当前运行态堆(Heap)中存在的实例整体状况来诊断问题。所有这些工具都有一个共同的特点,就是最终他们都需要通过和虚 拟机进行交互,来发现 Java 程序运行的问题。已有的这些工具虽然强大易用,但是在一些高级的应用环境中,开发者常常会有一些特殊的需求,这个时候就需要定制工具来达成目标。 JDK 本身定义了目标明确并功能完善的 API 来与虚拟机直接交互,而且这些 API 能很方便的进行扩展,从而满足开发者各式的需求。在本文中,将比较详细地介绍 JVMTI,以及如何使用 JVMTI 编写一个定制的 Agent 。AgentAgent 即 JVMTI 的客户端,它和执行 Java 程序的虚拟机运行在同一个进程上,因此通常他们的实现都很紧凑,他们通常由另一个独立的进程控制,充当这个独立进程和当前虚拟机之间的中介,通过调用 JVMTI 提供的接口和虚拟机交互,负责获取并返回当前虚拟机的状态或者转发控制命令。JVMTI 的简介JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的更新版本。从这个 API 的发展历史轨迹中我们就可以知道,JVMTI 提供了可用于 debug 和 profiler 的接口;同时,在 Java 5/6 中,虚拟机接口也增加了监听(Monitoring),线程分析(Thread analysis)以及覆盖率分析(Coverage Analysis)等功能。正是由于 JVMTI 的强大功能,它是实现 Java 调试器,以及其它 Java 运行态测试与分析工具的基础。JVMTI 并不一定在所有的 Java 虚拟机上都有实现,不同的虚拟机的实现也不尽相同。不过在一些主流的虚拟机中,比如 Sun 和 IBM,以及一些开源的如 Apache Harmony DRLVM 中,都提供了标准 JVMTI 实现。JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C+ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式),也可以在 Java 5 之后使用运行时加载(活动加载模式)。-agentlib:agent-lib-name=options-agentpath:path-to-agent=optionsAgent 的工作过程启动Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:所有的 Java 类都未被初始化;所有的对象实例都未被创建;因而,没有任何 Java 代码被执行;但在这个时候,我们已经可以:操作 JVMTI 的 Capability 参数;使用系统参数;动态库被加载之后,虚拟机会先寻找一个 Agent 入口函数:JNIEXPORTjintJNICALLAgent_OnLoad(JavaVM*vm,char*options,void*reserved)在这个函数中,虚拟机传入了一个 JavaVM 指针,以及命令行的参数。通过 JavaVM,我们可以获得 JVMTI 的指针,并获得 JVMTI 函数的使用能力,所有的 JVMTI 函数都通过这个 jvmtiEnv 获取,不同的虚拟机实现提供的函数细节可能不一样,但是使用的方式是统一的。jvmtiEnv*jvmti;(*jvm)-GetEnv(jvm,&jvmti,JVMTI_VERSION_1_0);这里传入的版本信息参数很重要,不同的 JVMTI 环境所提供的功能以及处理方式都可能有所不同,不过它在同一个虚拟机中会保持不变(有心的读者可以去比较一下 JNI 环境)。命令行参数事实上就是上面启动命令行中的 options 部分,在 Agent 实现中需要进行解析并完成后续处理工作。参数传入的字符串仅仅在 Agent_OnLoad 函数里有效,如果需要长期使用,开发者需要做内存的复制工作,同时在最后还要释放这块存储。另外,有些 JDK 的实现会使用 JAVA_TOOL_OPTIONS 所提供的参数,这个常见于一些嵌入式的 Java 虚拟机(不使用命令行)。需要强调的是,这个时候由于虚拟机并未完成初始化工作,并不是所有的 JVMTI 函数都可以被使用。Agent 还可以在运行时加载,如果您了解 Java Instrument 模块(可以参考这篇文章),您一定对它的运行态加载有印象,这个新功能事实上也是 Java Agent 的一个实现。具体说来,虚拟机会在运行时监听并接受 Agent 的加载,在这个时候,它会使用 Agent 的:JNIEXPORTjintJNICALLAgent_OnAttach(JavaVM*vm,char*options,void*reserved);同样的在这个初始化阶段,不是所有的 JVMTI 的 Capability 参数都处于可操作状态,而且 options 这个 char 数组在这个函数运行之后就会被丢弃,如果需要,需要做好保留工作。Agent 的主要功能是通过一系列的在虚拟机上设置的回调(callback)函数完成的,一旦某些事件发生,Agent 所设置的回调函数就会被调用,来完成特定的需求。卸载最后,Agent 完成任务,或者虚拟机关闭的时候,虚拟机都会调用一个类似于类析构函数的方法来完成最后的清理任务,注意这个函数和虚拟机自己的 VM_DEATH 事件是不同的。JNIEXPORTvoidJNICALLAgent_OnUnload(JavaVM*vm)JVMTI 的环境和错误处理我们使用 JVMTI 的过程,主要是设置 JVMTI 环境,监听虚拟机所产生的事件,以及在某些事件上加上我们所希望的回调函数。JVMTI 环境我们可以通过操作 jvmtiCapabilities 来查询、增加、修改 JVMTI 的环境参数。当然,对于每一个不同的虚拟机来说,基于他们的实现不尽相同,导致了 JVMTI 的环境也不一定一致。标准的 jvmtiCapabilities 定义了一系列虚拟机的功能,比如 can_redefine_any_class 定义了虚拟机是否支持重定义类,can_retransform_classes 定义了是否支持在运行的时候改变类定义等等。如果熟悉 Java Instrumentation,一定不会对此感到陌生,因为 Instrumentation 就是对这些在 Java 层上的包装。对用户来说,这块最主要的是查看当前 JVMTI 环境,了解虚拟机具有的功能。要了解这个,其实很简单,只需通过对 jvmtiCapabilities 的一系列变量的考察就可以。err=(*jvmti)-GetCapabilities(jvmti,&capa);/取得jvmtiCapabilities指针。if(err=JVMTI_ERROR_NONE)if(capa.can_redefine_any_class)./查看是否支持重定义类另外,虚拟机有自己的一些功能,一开始并未被启动,那么增加或修改 jvmtiCapabilities 也是可能的,但不同的虚拟机对这个功能的处理也不太一样,多数的虚拟机允许增改,但是有一定的限制,比如仅支持在 Agent_OnLoad 时,即虚拟机启动时作出,它某种程度上反映了虚拟机本身的构架。开发人员无需要考虑 Agent 的性能和内存占用,就可以在 Agent 被加载的时候启用所有功能:err=(*jvmti)-GetPotentialCapabilities(jvmti,&capa);/取得所有可用的功能if(err=JVMTI_ERROR_NONE)err=(*jvmti)-AddCapabilities(jvmti,&capa);.最后我们要注意的是,JVMTI 的函数调用都有其时间性,即特定的函数只能在特定的虚拟机状态下才能调用,比如 SuspendThread(挂起线程)这个动作,仅在 Java 虚拟机处于运行状态(live phase)才能调用,否则导致一个内部异常。VMTI 错误处理JVMTI 沿用了基本的错误处理方式,即使用返回的错误代码通知当前的错误,几乎所有的 JVMTI 函数调用都具有以下模式:jvmtiErrorerr=jvmti-someJVMTImethod(somePara);其中 err 就是返回的错误代码,不同函数的错误信息可以在 Java 规范里查到。JVMTI 基本功能JVMTI 的功能非常丰富,包含了虚拟机中线程、内存 / 堆 / 栈,类 / 方法 / 变量,事件 / 定时器处理等等 20 多类功能,下面我们介绍一下,并举一些简单列子。事件处理和回调函数从上文我们知道,使用 JVMTI 一个基本的方式就是设置回调函数,在某些事件发生的时候触发并作出相应的动作。因此这一部分的功能非常基本,当前版本的 JVMTI 提供了许多事件(Event)的回调,包括虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等。如果想对这些事件进行处理,我们需要首先为 该事件写一个函数,然后在 jvmtiEventCallbacks 这个结构中指定相应的函数指针。比如,我们对线程启动感兴趣,并写了一个 HandleThreadStart 函数,那么我们需要在 Agent_OnLoad 函数里加入:jvmtiEventCallbackseventCallBacks;memset(&ecbs,0,sizeof(ecbs);/初始化eventCallBacks.ThreadStart=&HandleThreadStart;/设置函数指针.在设置了这些回调之后,就可以调用下述方法,来最终完成设置。在接下来的虚拟机运行过程中,一旦有线程开始运行发生,虚拟机就会回调 HandleThreadStart 方法。jvmti-SetEventCallbacks(eventCallBacks,sizeof(eventCallBacks);设置回调函数的时候,开发者需要注意以下几点:如同 Java 异常机制一样,如果在回调函数中自己抛出一个异常(Exception),或者在调用 JNI 函数的时候制造了一些麻烦,让 JNI 丢出了一个异常,那么任何在回调之前发生的异常就会丢失,这就要求开发人员要在处理错误的时候需要当心。虚拟机不保证回调函数会被同步,换句话说,程序有可能同时运行同一个回调函数(比如,好几个线程同时开始运行了,这个 HandleThreadStart 就会被同时调用几次),那么开发人员在开发回调函数时需要处理同步的问题。内存控制和对象获取内存控制是一切运行态的基本功能。 JVMTI 除了提供最简单的内存申请和撤销之外(这块内存不受 Java 堆管理,开发人员需要自行进行清理工作,不然会造成内存泄漏),也提供了对 Java 堆的操作。众所周知,Java 堆中存储了 Java 的类、对象和基本类型(Primitive),通过对堆的操作,开发人员可以很容易的查找任意的类、对象,甚至可以强行执行垃圾收集工作。 JVMTI 中对 Java 堆的操作与众不同,它没有提供一个直接获取的方式(由此可见,虚拟机对对象的管理并非是哈希表,而是某种树 / 图方式),而是使用一个迭代器(iterater)的方式遍历:jvmtiErrorFollowReferences(jvmtiEnv*env,jintheap_filter,jclassklass,jobjectinitial_object,/该方式可以指定根节点constjvmtiHeapCallbacks*callbacks,/设置回调函数constvoid*user_data)或者jvmtiErrorIterateThroughHeap(jvmtiEnv*env,jintheap_filter,jclassklass,constjvmtiHeapCallbacks*callbacks,constvoid*user_data)/遍历整个heap在遍历的过程中,开发者可以设定一定的条件,比如,指定是某一个类的对象,并设置一个回调函数,如果条件被满足,回调函数就会被执行。开发者可以在 回调函数中对当前传回的指针进行打标记(tag)操作这又是一个特殊之处,在第一遍遍历中,只能对满足条件的对象进行 tag ;然后再使用 GetObjectsWithTags 函数,获取需要的对象。jvmtiErrorGetObjectsWithTags(jvmtiEnv*env,jinttag_count,constjlong*tags,/设定特定的tag,即我们上面所设置的jint*count_ptr,jobject*object_result_ptr,jlong*tag_result_ptr)如果你仅仅想对特定 Java 对象操作,应该避免设置其他类型的回调函数,否则会影响效率,举例来说,多增加一个 primitive 的回调函数,可能会使整个操作效率下降一个数量级。线程和锁线程是 Java 运行态中非常重要的一个部分,在 JVMTI 中也提供了很多 API 进行相应的操作,包括查询当前线程状态,暂停,恢复或者终端线程,还可以对线程锁进行操作。开发者可以获得特定线程所拥有的锁:jvmtiErrorGetOwnedMonitorInfo(jvmtiEnv*env,jthreadthread,jint*owned_monitor_count_ptr,jobject*owned_monitors_ptr)也可以获得当前线程正在等待的锁:jvmtiErrorGetCurrentContendedMonitor(jvmtiEnv*env,jthreadthread,jobject*monitor_ptr)知道这些信息,事实上我们也可以设计自己的算法来判断是否死锁。更重要的是,JVMTI 提供了一系列的监视器(Monitor)操作,来帮助我们在 native 环境中实现同步。主要的操作是构建监视器(CreateRawMonitor),获取监视器(RawMonitorEnter),释放监视器 (RawMonitorExit),等待和唤醒监视器 (RawMonitorWait,RawMonitorNotify) 等操作,通过这些简单锁,程序的同步操作可以得到保证。调试功能调试功能是 JVMTI 的基本功能之一,这主要包括了设置断点、调试(step)等,在 JVMTI 里面,设置断点的 API 本身很简单:jvmtiErrorSetBreakpoint(jvmtiEnv*env,jmethodIDmethod,jlocationlocation)jlocation 这个数据结构在这里代表的是对应方法方法中一个可执行代码的行数。在断点发生的时候,虚拟机会触发一个事件,开发者可以使用在上文中介绍过的方式对事件进行处理。JVMTI 数据结构JVMTI 中使用的数据结构,首先也是一些标准的 JNI 数据结构,比如 jint,jlong ;其次,JVMTI 也定义了一些基本类型,比如 jthread,表示一个 thread,jvmtiEvent,表示 jvmti 所定义的事件;更复杂的有 JVMTI 的一些需要用结构体表示的数据结构,比如堆的信息(jvmtiStackInfo)。这些数据结构在文档中都有清楚的定义,本文就不再详细解释。一个简单的 Agent 实现下面将通过一个具体的例子,来阐述如何开发一个简单的 Agent 。这个 Agent 是通过 C+ 编写的(读者可以在最后下载到完整的代码),他通过监听 JVMTI_EVENT_METHOD_ENTRY 事件,注册对应的回调函数来响应这个事件,来输出所有被调用函数名。有兴趣的读者还可以参照这个基本流程,通过 JVMTI 提供的丰富的函数来进行扩展和定制。Agent 的设计具体实现都在 MethodTraceAgent 这个类里提供。按照顺序,他会处理环境初始化、参数解析、注册功能、注册事件响应,每个功能都被抽象在一个具体的函数里。classMethodTraceAgentpublic:voidInit(JavaVM*vm)constthrow(AgentException);voidParseOptions(constchar*str)constthrow(AgentException);voidAddCapability()constthrow(AgentException);voidRegisterEvent()constthrow(AgentException);.private:.staticjvmtiEnv*m_jvmti;staticchar*m_filter;Agent_OnLoad 函数会在 Agent 被加载的时候创建这个类,并依次调用上述各个方法,从而实现这个 Agent 的功能。JNIEXPORTjintJNICALLAgent_OnLoad(JavaVM*vm,char*options,void*reserved).MethodTraceAgent*agent=newMethodTraceAgent();agent-Init(vm);agent-ParseOptions(options);agent-AddCapability();agent-RegisterEvent();.运行过程如图 1 所示:图 1. Agent 时序图Agent 编译和运行Agent 的编译非常简单,他和编译普通的动态链接库没有本质区别,只是需要将 JDK 提供的一些头文件包含进来。Windows: cl/EHsc-I$JAVA_HOMEinclude-I$JAVA_HOMEincludewin32-LDMethodTraceAgent.cppMain.cpp-FeAgent.dllLinux: g+-I$JAVA_HOME/include/-I$JAVA_HOME/include/linuxMethodTraceAgent.cppMain.cpp-fPIC-shared-olibagent.so在附带的代码文件里提供了一个可运行的 Java 类,默认情况下运行的结果如下图所示:图 2. 默认运行输出现在,我们运行程序前告诉 Java 先加载编译出来的 Agent:java-agentlib:Agent=firstMethodTraceTest这次的输出如图 3. 所示:图 3. 添加 Agent 后输出可以当程序运行到到 MethodTraceTest 的 first 方法是,Agent 会输出这个事件。“ first ”是 Agent 运行的参数,如果不指定话,所有的进入方法的触发的事件都会被输出,如果读者把这个参数去掉再运行的话,会发现在运行 main 函数前,已经有非常基本的类库函数被调用了。结语Java 虚拟机通过 JVMTI 提供了一整套函数来帮助用户检测管理虚拟机运行态,它主要通过 Agent 的方式实现与用户的互操作。本文简单介绍了 Agent 的实现方式和 JVMTI 的使用。通过 Agent 这种方式不仅仅用户可以使用,事实上,JDK 里面的很多工具,比如 Instrumentation 和 JDI, 都采用了这种方式。这种方式无需把这些工具绑定在虚拟机上,减少了虚拟机的负荷和内存占用。在下一篇中,我们将介绍 JDWP 如何采用 Agent 的方式,定义自己的一套通信原语,并通过多种通信方式,对外提供基本调试功能。深入 Java 调试体系,第 3 部分: JDWP 协议及实现JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI),本系列将会详细介绍这三个模块的内部细节、通过实例为读者揭开 JPDA 的面纱。本文是该系列的第三篇。用户在使用 Java 语言开发应用程序的时候,往往需要对虚拟机内部的运行状态进行观察和调试,那么调试器是如何对 Java 虚拟机中的信息进行观察的呢?这篇文章将会着重介绍 JDWP 协议的具体细节,并通过讲解一个 JDWP 的命令以及剖析程序调试过程中断点的生成过程来为读者揭示 JDWP 的实现机制。JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议。JDWP 协议介绍这里首先要说明一下 debugger 和 target vm。Target vm 中运行着我们希望要调试的程序,它与一般运行的 Java 虚拟机没有什么区别,只是在启动时加载了 Agent JDWP 从而具备了调试功能。而 debugger 就是我们熟知的调试器,它向运行中的 target vm 发送命令来获取 target vm 运行时的状态和控制 Java 程序的执行。Debugger 和 target vm 分别在各自的进程中运行,他们之间的通信协议就是 JDWP。JDWP 与其他许多协议不同,它仅仅定义了数据传输的格式,但并没有指定具体的传输方式。这就意味着一个 JDWP 的实现可以不需要做任何修改就正常工作在不同的传输方式上(在 JDWP 传输接口中会做详细介绍)。JDWP 是语言无关的。理论上我们可以选用任意语言实现 JDWP。然而我们注意到,在 JDWP 的两端分别是 target vm 和 debugger。Target vm 端,JDWP 模块必须以 Agent library 的形式在 Java 虚拟机启动时加载,并且它必须通过 Java 虚拟机提供的 JVMTI 接口实现各种 debug 的功能,所以必须使用 C/C+ 语言编写。而 debugger 端就没有这样的限制,可以使用任意语言编写,只要遵守 JDWP 规范即可。JDI(Java Debug Interface)就包含了一个 Java 的 JDWP debugger 端的实现(JDI 将在该系列的下一篇文章中介绍),JDK 中调试工具 jdb 也是使用 JDI 完成其调试功能的。图 1. JDWP agent 在调试中扮演的角色协议分析JDWP 大致分为两个阶段:握手和应答。握手是在传输层连接建立完成后,做的第一件事:Debugger 发送 14 bytes 的字符串“JDWP-Handshake”到 target Java 虚拟机Target Java 虚拟机回复“JDWP-Handshake”图 2. JDWP 的握手协议握手完成,debugger 就可以向 target Java 虚拟机发送命令了。JDWP 是通过命令(command)和回复(reply)进行通信的,这与 HTTP 有些相似。JDWP 本身是无状态的,因此对 command 出现的顺序并不受限制。JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。Debugger 和 target Java 虚拟机都有可能发送 command 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。Packet 的结构Packet 分为包头(header)和数据(data)两部分组成。包头部分的结构和长度是固定,而数据部分的长度是可变的,具体内容视 packet 的内容而定。Command packet 和 reply packet 的包头长度相同,都是 11 个 bytes,这样更有利于传输层的抽象和实现。Command packet 的 header 的结构 :图 3. JDWP command packet 结构Length 是整个 packet 的长度,包括 length 部分。因为包头的长度是固定的 11 bytes,所以如果一个 command packet 没有数据部分,则 length 的值就是 11。Id 是一个唯一值,用来标记和识别 reply 所属的 command。Reply packet 与它所回复的 command packet 具有相同的 Id,异步的消息就是通过 Id 来配对识别的
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年文化创意产业版权质押贷款合同
- 2025年房地产项目预付款合同规范文本
- 二零二五年度金融企业不良资产债务收购与处置合同
- 2025年绿色生态住宅装修设计施工一体化合同范本解读
- 2025年农业合作社与加工企业订单种植合同范本
- 手术前后病人的护理试题及答案
- 2025中学清明节系列活动方案
- 2025版幼儿园劳动教育五一劳动节系列活动方案
- 2024年院感考试题(附答案)
- 危急值试题及答案
- 孕产妇营养指导与咨询制度
- 肝豆状核变性课件
- 种植牙二期修复
- 新进人员院感培训
- 2024年外包合同模板(通用)(附件版)
- 妇科质控中心半年工作总结
- 沥青路面工程监理实施细则
- 《快消品行业分析》课件
- 英语10000个单词频率排序
- 人民调解工作方法与技巧
- 传染病标本的采集、保存、运送管理规范
评论
0/150
提交评论