7.5.5 Java虚拟机工具接口

前面介绍了使用虚拟机中JMX API中的MBean来对虚拟机进行监控和管理。不过可以通过MBean来进行监控和管理的内容只是虚拟机所提供的功能中的很小一部分,其中大部分功能是无法通过Java API来访问的。这主要是因为对于运行在虚拟机上的Java程序来说,其他的监控和管理功能可以使用的场景非常少。虚拟机提供的所有监控和管理功能,虽然不能通过Java API来直接使用,但是可以通过C/C++原生代码来使用。J2SE 5.0对虚拟机中与监控和管理相关的功能进行整合,形成了标准的Java虚拟机工具接口(Java Virtual Machine Tools Interface, JVM TI)。

1.基本使用

与JMX API不同,JVM TI主要是供开发、调试和监控工具使用的,并不是针对普通的应用程序而设计的。使用JVM TI可以查看和控制当前在虚拟机上运行的程序的状态。通过使用JVM TI,可以开发出各种面向虚拟机上运行程序的工具,实现包括程序调试、性能分析、运行状态监控、线程分析和代码覆盖率分析等功能。不过这些工具只能使用原生代码来实现。

使用JVM TI开发的工具被称为虚拟机上的代理程序(agent)。在虚拟机启动时,代理程序也会被加载和运行。当Java程序在虚拟机上运行时,代理程序可以通过JVM TI查看和控制Java程序中的相关状态。当虚拟机退出时,代理程序也会相应退出。代理程序和虚拟机在同一个进程中运行。代理程序本身是底层操作系统平台上的一个原生代码库,例如在Windows平台上是一个DLL文件。在虚拟机启动时,通过参数“-agentlib”或“-agentpath”来指定原生代码库的名称或绝对路径。如果指定的是代码库的名称,则由虚拟机根据名称自动进行搜索。在指定代理程序名称或路径的时候,可以指定额外的配置参数。例如,“-agentlib:myAgent=option1,option2”指明了代理程序的名称是myAgent,同时传入两个额外的参数option1和option2。

2.使用案例

下面通过一个具体的案例来说明JVM TI的用法。这个案例中的工具的作用是统计Java程序在运行过程中不同方法的调用次数。方法调用次数的信息对于分析程序中的性能瓶颈是很重要的。代理程序可以通过两种方式来使用JVM TI提供的功能。第一种是方法调用,用来进行直接的状态查询和更新;第二种是注册事件监听器,用来在虚拟机中特定的事件发生时执行相关的逻辑。对于这个案例来说,主要采用的是注册事件监听器的方式。当虚拟机开始执行某个方法时,会产生相关的事件,只需要在这个事件的处理方法中根据当前方法的名称进行统计即可。

在代理程序的C++代码中需要添加对JDK中include目录下的jvmti.h头文件的引用。这个头文件中包含JVM TI中的类型声明和方法定义。代理程序被加载之后,其中的Agent_OnLoad方法会被调用。Agent_OnLoad方法也是代理程序运行的起点。Agent_OnLoad方法的声明是固定的,如代码清单7-27所示。JavaVM类型的参数表示的是当前的虚拟机对象,而参数options则是之前提到的通过虚拟机启动参数传递给代理程序的额外参数。在Agent_OnLoad方法被调用时,虚拟机本身的初始化还没有完成,因此在Agent_OnLoad方法中所能执行的操作是有限的。通常在Agent_OnLoad方法中注册事件监听器。

虚拟机上的所有事件默认是禁用的。要启用某个事件,通常要三步:首先启用某些事件所依赖的虚拟机本身的支持能力。JVM TI中所规范的监控和管理的功能并不是在所有虚拟机实现上都可用的。这些功能分为必需和可选两类。所有虚拟机都应该实现必需的功能,而可选功能在某些虚拟机上可能并不支持。本案例所需的在进入方法调用时产生的事件JVMTI_EVENT_METHOD_ENTRY是一个可选功能。对于可选功能,需要通过JVM TI的AddCapabilities方法显式地启用。在启用了事件相关的功能之后,接着需要启用该事件。只需要使用SetEventNotificationMode方法把事件的状态设为JVMTI_ENABLE即可。最后一步是注册事件的处理方法。事件发生时的回调方法由JVM TI中的jvmtiEventCallbacks结构来表示,在其中可以设置所监听的各种事件对应的回调方法。本案例只对JVMTI_EVENT_METHOD_ENTRY事件感兴趣,因此只通过MethodEntry属性设置了该事件的回调方法,其值是一个函数指针。在填充完jvmtiEventCallbacks结构之后,使用SetEventCallbacks方法来注册事件监听器。

代码清单7-27 代理程序的Agent_OnLoad方法


JNIEXPORT jint JNICALL Agent_OnLoad(JavaVMjvm, charoptions, void*reserved)

{

jvmtiEnv*jvmti;

jvm->GetEnv((void**)&jvmti, JVMTI_VERSION_1_0);

jvmtiCapabilities capa;

memset(&capa,0,sizeof(jvmtiCapabilities));

capa.can_generate_method_entry_events=1;

jvmti->AddCapabilities(&capa);

jvmtiEventCallbacks callbacks;

callbacks.MethodEntry=&Method_Entry;

jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);

jvmti->SetEventCallbacks(&callbacks,(jint)sizeof(callbacks));

return JNI_OK;

}


代码清单7-27中的jvmtiEnv类型的变量表示的是JVM TI的运行环境。代理程序通过此变量与JVM TI进行交互。对于方法进入事件JVMTI_EVENT_METHOD_ENTRY的处理函数如代码清单7-28所示。当虚拟机调用某个方法的时候,代理程序中的Method_Entry函数会被调用。通过函数的参数可以获取表示当前调用方法的jmethodID对象。Method_Entry函数中的逻辑比较简单,通过jvmtiEnv提供的功能来获取方法所在的类的名称和方法的名称,并把统计数据保存在一个哈希表中。这里需要注意的一点是,通过调用jvmtiEnv中方法所得到的字符串,都需要在使用完毕之后调用Deallocate方法来释放。

代码清单7-28 方法进入事件处理函数


map<string, int>methodCountMap;

void JNICALL Method_Entry(jvmtiEnvjvmti_env, JNIEnvjni_env, jthread thread, jmethodID method)

{

char*method_name;

jclass cls;

char*class_signature;

jvmti_env->GetMethodName(method,&method_name, NULL, NULL);

jvmti_env->GetMethodDeclaringClass(method,&cls);

jvmti_env->GetClassSignature(cls,&class_signature, NULL);

char name[256]={0};

strcpy(name, class_signature);

strcat(name, method_name);

methodCountMap[name]++;

jvmti_env->Deallocate((unsigned char*)method_name);

jvmti_env->Deallocate((unsigned char*)class_signature);

}


与Agent_OnLoad方法相对应,当虚拟机完成程序的执行并准备退出之前,会调用代理程序中的Agent_OnUnload方法。代码清单7-29给出了案例中的Agent_OnUnload方法的实现,其作用是把统计出来的方法调用次数的数据输出到一个文件中。

代码清单7-29 代理程序的Agent_OnUnload方法


JNIEXPORT void JNICALL Agent_OnUnload(JavaVM*vm)

{

ofstream file;

file.open("C:\method_trace.txt",ios:out);

for(map<string, int>:iterator iter=methodCountMap.begin();iter!=methodCountMap.end();++iter)

{

file<<(iter).first<<"\t"<<(iter).second<<endl;

}

file.close();

}


在编译和链接C++程序之后,可以得到代理程序的原生代码库。在启动虚拟机执行任何Java程序的时候都可以加载该代理程序。当程序运行完毕之后,可以从输出的文件中看到程序中所有方法的调用次数的统计信息。