浅谈JPDA中JVMTI模块

0 前言

上一节《Java Instrument 功能使用及原理》文章中,讲解Instrument使用时,简单提了一句JVMTI的概念,可能有不少小伙伴感到很陌生,虽然Java Instrument的使用基本没什么问题,但对于Instrument基于JVMTI的实现原理仍是处于混沌状态。因此本节的重点就在于讲解JVMTI,正如标题。前端

但因为JVMTI在整个JVM JPDA体系中只是其中的一个小模块,为了使你们在总体上能有个清晰的认识,那咱们先从JPDA体系开始吧。java

1 JPDA 介绍

全部的程序员都会遇到 bug,对于运行态的错误,咱们每每须要一些方法来观察和测试运行态中的环境。做为一个合格的Developer,最基本的技能就是要掌握语言在不一样IDE的Debug技能。linux

Intellij IDEA 就提供一个功能很是全面,操做很是简单的调试器,以下图:c++

Intellij IDEA 调试界面

有时甚至不用 IDE 提供的图形界面,使用 JDK 自带的 jdb 工具,以文本命令的形式来调试您的 Java 程序。这些形形色色的调试器都 支持本地和远程的程序调试,那么它们是如何被开发的?它们之间存在着什么样的联系呢?程序员

咱们不得不说起 Java 的调试体系—— JPDA,它是咱们通向虚拟机,考察虚拟机运行态的一个通道,一套工具算法

Java 程序都是运行在 Java 虚拟机上的,咱们要调试 Java 程序,事实上就须要向 Java 虚拟机请求当前运行态的状态,并对虚拟机发出必定的指令,设置一些回调等等,那么 Java 的调试体系——JPDA,就是虚拟机的一整套用于调试的工具和接口编程

顾名思义,这个体系为开发人员提供了 一整套用于调试 Java 程序的 API,是一套用于开发 Java 调试工具的接口和协议windows

经过这些 JPDA 提供的接口和协议,调试器开发人员就能根据特定开发者的需求,扩展定制 Java 调试应用程序,开发出吸引开发人员使用的调试工具。后端

但咱们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准,所以,经过 JPDA 开发出来的调试工具先天 具备跨平台、不依赖虚拟机实现、JDK 版本无关等移植优势,所以大部分的调试工具都是基于这个体系。api

1.1 JPDA 模块

JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,并且规定了它们三者之间的交互方式,或者说定义了它们通讯的接口。

三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI)。这三个模块把调试过程分解成几个很天然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通讯器

被调试者(JVMTI):运行于咱们想调试的 Java 虚拟机之上,它能够经过 JVMTI 这个标准接口,监控当前虚拟机的信息

调试者(JDI):定义了用户可以使用的调试接口,经过这些接口,用户能够对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果

中间通讯器(JDWP)在调试者和被调试者之间,调试命令和调试结果,都是经过 JDWP 的通信协议传输的;全部的命令被封装成 JDWP 命令包,经过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行;相似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是经过 JDI 获得数据,发出指令;

上述整个过程,以下图所示:

JPDA 模块层次

固然,开发人员彻底能够不使用完整的三个层次,而是 基于其中的某一个层次开发本身的应用。好比:彻底能够仅仅依靠经过 JVMTI 函数开发一个调试工具,而不使用 JDWP 和 JDI,只使用本身的通信和命令接口。固然,除非是有特殊的需求,利用已有的实现会事半功倍,避免重复发明轮子。

下面,咱们就分别讲解下JPDA的三种组成模块:

  1. Java 虚拟机工具接口(JVMTI)

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

  2. 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 的标准本身实现

  3. Java 调试接口(JDI)

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

JPDA 层次比较

1.2 JPDA 实现

每个虚拟机都应该实现 JVMTI 接口,可是 JDWP 和 JDI 自己与虚拟机并不是是不可分的,这三个层之间是经过标准所定义的交互的接口和协议联系起来的,所以它们能够被独立替换或取代,但不会影响到总体调试工具的开发和使用。所以,开发和使用本身的 JDWP 和 JDI 接口实现是可能的。

Java 软件开发包(SDK)标准版里提供了 JPDA 三个层次的标准实现,事实上,调试工具开发人员还有不少其余开源实现能够选择,好比 Apache Harmony 提供了 JDWP 的实现。而 JDI,咱们能够在 Eclipse 一个子项目 org.eclipse.jdt.debug 里找到其完整的实现(Harmony 也使用了这套实现,做为其 J2SE 类库的一部分)。经过标准协议,Eclipse IDE 的调试工具就能够彻底在 Harmony 的环境上运行。

2 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 中,JVMTI 接口也增长了 监听(Monitoring),线程分析(Thread analysis)以及覆盖率分析(Coverage Analysis) 等功能。正是因为 JVMTI 的强大功能,它是实现 Java 调试器,以及其它 Java 运行态测试与分析工具的基础。

JVMTI 是一套本地代码接口,可使开发者直接与 C/C++ 以及 JNI 打交道

那么,开发者是如何来使用JVMTI所提供的接口呢?事实上,通常采用创建一个 Agent 的方式来使用 JVMTI,这个Agent的表现形式是一个以c/c++语言编写的动态连接库

把 Agent 编译成一个动态连接库,Java启动或运行时,动态加载一个外部基于JVMTI 编写的dynamic module到Java进程内,而后触发 JVM源生线程Attach Listener来执行这个dynamic module的回调函数

在回调函数体内,能够 获取各类各样的VM级信息,注册感兴趣的VM事件,甚至控制VM行为

2.1 Agent 工做过程

2.1.1 启动

JVMTI有两种启动方式,第一种是随Java进程启动时,自动载入共享库,下文简称 启动时载入。另外一种方式是,Java运行时,经过attach api动态载入,下文简称 运行时载入

启动时载入,经过在Java命令行启动时传递一个特殊的option,以下:

  1. java -agentlib:= Sample 注意,这里的共享库路径是环境变量路径,例如 java -agentlib:foo=opt1,opt2,java启动时会从linux的LD_LIBRARY_PATH或windows的PATH环境变量定义的路径处装载foo.so或foo.dll,找不到则抛异常

  2. java -agentpath:= Sample 这是 以绝对路径的方式装载共享库,例如 java -agentpath:/home/admin/agentlib/foo.so=opt1,opt2

启动时载入,处于虚拟机初始化的早期,在这个时间点上:

  1. 全部的 Java 类都未被初始化;
  2. 全部的 Java 对象实例都未被建立;
  3. 于是,没有任何 Java 代码被执行;

但在这个时候,咱们已经能够:

  1. 操做 JVMTI 的 Capability 参数;
  2. 使用系统参数;

动态库被加载以后,虚拟机会先寻找一个 Agent 入口函数:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
复制代码

在这个Agent_OnLoad函数中,虚拟机传入了三个参数:

  1. JavaVM *vmJVM上下文经过 JavaVM,能够得到 JVMTI 的指针,并得到 JVMTI 函数的使用能力;
  2. char *options外部传入的参数,好比上面例子中给的 opt1, opt2,它仅仅是一个字符串
  3. void *reserved:一个预留参数,没必要关心它;

运行时载入,经过attach api,这是一套纯Java的API,它负责动态地将dynamic module attach到指定进程id的Java进程内并触发回调。例子以下:

import java.io.IOException;
import com.sun.tools.attach.VirtualMachine;

public class VMAttacher {
    public static void main(String[] args) throws Exception {
         // args[0]为java进程id
         VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(args[0]);
         // args[1]为共享库路径,args[2]为传递给agent的参数
         virtualMachine.loadAgentPath(args[1], args[2]);
         virtualMachine.detach();
    }
}
复制代码

Attach API位于$JAVA_HOME/lib/tools.jar,因此在编译时,须要将这个jar放入classpath。例如:

javac -cp $JAVA_HOME/lib/tools.jar VMAttacher.java pid /home/admin/agentlib/foo.so opt1,opt2
复制代码

运行时载入,虚拟机会在运行时监听并接受 Agent 的加载,在这个时候,它会使用 Agent 的:

JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *options, void *reserved);
复制代码

2.1.2 卸载

最后,Agent 完成任务,或者虚拟机关闭的时候,虚拟机都会调用一个相似于类析构函数的方法来完成最后的清理任务

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
复制代码

2.2 JVMTI 环境和错误处理

使用 JVMTI 的过程,主要是 设置 JVMTI 环境,监听虚拟机所产生的事件,以及在某些事件上加上回调函数

2.2.1 JVMTI 环境

经过操做 jvmtiCapabilities 来查询、增长、修改 JVMTI 的环境参数。标准的 jvmtiCapabilities 定义了一系列虚拟机的功能,好比:

  1. can_redefine_any_class:定义了虚拟机是否支持重定义类;
  2. can_retransform_classes:定义了虚拟机是否支持运行的时候改变类定义;

若是熟悉 Java Instrumentation,必定不会对此感到陌生,由于 Instrumentation 就是对这些在 Java 层上的包装。对用户来讲,只需得到 jvmtiCapabilities指针,就能够查看当前 JVMTI 环境,了解虚拟机具备的一系列变量功能

// 取得 jvmtiCapabilities 指针
err = (*jvmti)->GetCapabilities(jvmti, &capa);
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)才能调用,不然致使一个内部异常。

2.2.2 JVMTI 错误处理

JVMTI 沿用了基本的错误处理方式,即便用返回的错误代码通知当前的错误,几乎全部的 JVMTI 函数调用都具备如下模式:

jvmtiError err = jvmti->someJVMTImethod (somePara … );
复制代码

其中 err 就是返回的错误代码,不一样函数的错误信息能够在 Java 规范里查到。

2.3 JVMTI 基本功能

JVMTI 的功能很是丰富,包含了 虚拟机中线程、内存 / 堆 / 栈,类 / 方法 / 变量,事件 / 定时器处理等等 20 多类功能,从功能上大体能够分为4类,以下:

  1. Heap:获取全部类的信息,对象信息,对象引用关系,Full GC开始/结束,对象回收事件等;
  2. 线程与堆栈:获取全部线程的信息,线程组信息,控制线程(start,suspend,resume,interrupt…), Thread Monitor(Lock),获得线程堆栈,控制出栈,方法强制返回,方法栈本地变量等;
  3. Class & Object & Method & Field 元信息:class信息,符号表,方法表,redefine class(hotswap), retransform class,object信息,fields信息,method信息等;
  4. 工具类:线程cpu消耗,classloader路径修改,系统属性获取等;

2.3.1 事件处理和回调函数

从上文咱们知道,使用 JVMTI 一个基本的方式就是设置回调函数,在某些事件发生的时候触发并做出相应的动做

所以这一部分的功能很是基本,当前版本的 JVMTI 提供了许多事件(Event)的回调,包括 虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等。若是想对这些事件进行处理,须要首先为该事件写一个函数,而后在 jvmtiEventCallbacks 这个结构中指定相应的函数指针

好比:咱们对线程启动感兴趣,并写了一个 HandleThreadStart 函数,那么咱们须要在 Agent_OnLoad 函数里加入:

jvmtiEventCallbacks eventCallBacks; 
// 初始化
memset(&ecbs, 0, sizeof(ecbs));
// 设置函数指针
eventCallBacks.ThreadStart = &HandleThreadStart;
...
复制代码

在设置了这些回调以后,就能够调用下述方法,来最终完成设置。在接下来的虚拟机运行过程当中,一旦有线程开始运行发生,虚拟机就会回调 HandleThreadStart 方法。

jvmti->SetEventCallbacks(eventCallBacks, sizeof(eventCallBacks));
复制代码

设置回调函数的时候,开发者须要注意如下几点

  1. 如同 Java 异常机制同样,若是在回调函数中本身抛出一个异常(Exception),或者在调用 JNI 函数的时候制造了一些麻烦,让 JNI 丢出了一个异常,那么 任何在回调以前发生的异常就会丢失,这就要求开发人员要在处理错误的时候须要小心
  2. 虚拟机不保证回调函数会被同步,换句话说,程序有可能同时运行同一个回调函数(好比,好几个线程同时开始运行了,这个 HandleThreadStart 就会被同时调用几回),那么开发人员在开发回调函数时须要处理同步的问题

2.3.2 内存控制和对象获取

内存控制是一切运行态的基本功能。 JVMTI 除了提供最简单的内存申请和撤销以外(这块内存不受 Java 堆管理,开发人员须要自行进行清理工做,否则会形成内存泄漏),也提供了对 Java 堆的操做。

众所周知,Java 堆中存储了 Java 的类、对象和基本类型(Primitive),经过对堆的操做,开发人员能够很容易的查找任意的类、对象,甚至能够强行执行垃圾收集工做

JVMTI 中对 Java 堆的操做不同凡响,它没有提供一个直接获取的方式(因而可知,虚拟机对对象的管理并不是是哈希表,而是某种树 / 图方式),而是使用一个迭代器(iterater)的方式遍历:

jvmtiError FollowReferences(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    jobject initial_object,// 该方式能够指定根节点
    const jvmtiHeapCallbacks* callbacks,// 设置回调函数
    const void* user_data)
复制代码

或者

jvmtiError IterateThroughHeap(jvmtiEnv* env, 
    jint heap_filter, 
    jclass klass, 
    const jvmtiHeapCallbacks* callbacks, 
    const void* user_data)// 遍历整个 heap
复制代码

在遍历的过程当中,开发者能够设定必定的条件,好比:指定是某一个类的对象,并设置一个回调函数,若是条件被知足,回调函数就会被执行。开发者能够在回调函数中对当前传回的指针进行打标记(tag)操做——这又是一个特殊之处,在第一遍遍历中,只能对知足条件的对象进行 tag ;而后再使用 GetObjectsWithTags 函数,获取须要的对象。

jvmtiError GetObjectsWithTags(jvmtiEnv* env, 
    jint tag_count, 
    const jlong* tags, // 设定特定的 tag,即咱们上面所设置的
    jint* count_ptr, 
    jobject** object_result_ptr, 
    jlong** tag_result_ptr)
复制代码

若是你仅仅想对特定 Java 对象操做,应该避免设置其余类型的回调函数,不然会影响效率,举例来讲,多增长一个 primitive 的回调函数,可能会使整个操做效率降低一个数量级。

2.3.3 线程和锁

线程是 Java 运行态中很是重要的一个部分,在 JVMTI 中也提供了不少 API 进行相应的操做,包括查询当前线程状态,暂停,恢复或者终端线程,还能够对线程锁进行操做。

开发者能够得到特定线程所拥有的锁:

jvmtiError GetOwnedMonitorInfo(jvmtiEnv* env, 
    jthread thread, 
    jint* owned_monitor_count_ptr, 
    jobject** owned_monitors_ptr)
复制代码

也能够得到当前线程正在等待的锁:

jvmtiError GetCurrentContendedMonitor(jvmtiEnv* env, 
    jthread thread, 
    jobject* monitor_ptr)
复制代码

知道这些信息,事实上咱们也能够设计本身的算法来判断是否死锁。更重要的是,JVMTI 提供了一系列的监视器(Monitor)操做,来帮助咱们在 native 环境中实现同步。

主要操做:构建监视器(CreateRawMonitor),获取监视器(RawMonitorEnter),释放监视器(RawMonitorExit),等待和唤醒监视器 (RawMonitorWait,RawMonitorNotify) 等操做,经过这些简单锁,程序的同步操做能够获得保证。

2.3.4 调试功能

调试功能是 JVMTI 的基本功能之一,这主要包括了设置断点、调试(step)等,在 JVMTI 里面,设置断点的 API 自己很简单:

jvmtiError SetBreakpoint(jvmtiEnv* env, 
    jmethodID method, 
    jlocation location)
复制代码

jlocation 这个数据结构在这里表明的是对应方法中一个可执行代码的行数。在断点发生的时候,虚拟机会触发一个事件,开发者可使用在上文中介绍过的方式对事件进行处理。

2.3.5 JVMTI 数据结构

JVMTI 中使用的数据结构,首先也是一些标准的 JNI 数据结构,好比:jint,jlong ;其次,JVMTI 也定义了一些基本类型,好比:jthread,表示一个 thread,jvmtiEvent,表示 jvmti 所定义的事件;更复杂的有 JVMTI 的一些须要用结构体表示的数据结构,好比:堆的信息(jvmtiStackInfo)。

相关文章
相关标签/搜索