服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

使用java虚拟机工具接口创建调试和分析代理 (1)

【赛迪网-it技术报道】java 虚拟机工具接口提供了一种编程接口,允许软件开发人员创建软件代理以监视和控制 java 编程语言应用程序。jvmti是java 2 software development kit (sdk), standard edition, 版本1.5.0 中的一种新增功能。它取代了 java virtual machine profiling interface (jvmpi),从版本 1.1 起即作为 java 2 sdk 的一种实验功能包括在内。在 jsr-163 中对 jvmti 进行了有关说明。

本文阐述如何使用 jvmti 创建 java 应用程序的调试和分析工具。这种工具(也称作代理)在应用程序中发生事件时,能够使用该接口提供的功能对事件通知进行注册,并查询和控制该应用程序。这里提供了 jvmti 的文档资料。jvmti 代理对于调试和调优应用程序十分有用。它可以对应用程序的各个方面予以说明,如内存分配情况、cpu 利用情况及锁争夺情况。

尽管jvmpi现在仍处于实验阶段,很多java技术开发人员已经在使用它了,而且
已经把它应用到多种市场上提供的java应用程序profiler。
请注意,极力鼓励开发人员使用jvmti而不使用jvmpi。jvmpi在不久的将来将被废止。

jvmti 在多个方面改进了 jvmpi 的功能和性能。例如:

☆jvmti依赖于每个事件的回调。这比 jvmpi 设计使用需要编组和取消编组的事件结构更有效。

☆jvmti包含四倍于 jvmpi 的函数(包括用于获取关于变量、字段、方法和类的信息的更多函数)。有关jvmti函数的完整索引,请参见函数索引页。

☆jvmti比jvmpi提供更多类型的事件通知,包括异常事件、字段访问和修改事件、断点和单步骤事件等。

☆有些从未被充分利用的jvmpi事件,如 arena 的 new 和 delete,或者通过字节码工具很容易就能获得的内容,或者 jvmti 函数本身(如 heap dump 和 object allocation)往往被 丢掉。 对这些事件的描述位于事件索引页。

☆jvmti是基于功能的,而 jvmpi 对于相应性能影响却是“要么全有,要么全无”。

☆jvmpi堆功能不可伸缩。

☆jvmpi没有错误返回信息。

☆jvmpi在 vm 实现方面具有很强的侵入性,容易导致维护问题和性能受损。

☆jvmpi是个实验产品,不久将废止。

在本文的以下部分,我们介绍一个简单代理,它使用 jvmti 函数从 java 应用程序提取信息。 代理的编写必须使用本地代码。这里给出的示例代理是使用 c 语言编写的。您可以于此下载完整的示例代理代码。下面几段介绍如何初始化一个代理,以及代理如何使用 jvmti 函数提取关于 java 应用程序的信息,以及如何编译和运行代理。此示例代码和编译步骤特定于 unix 环境,但是经过修改后也可用于 windows。这里介绍的代理可用于在任何 java 应用程序中分析线程和确定 jvm 内存使用情况。

这里包含一个用 java 语言编写的简单程序,称作 simplethread.java,并可从这里下载。我们使用 threadsample.java 演示此代理的预期输出。

jvmti 的功能很多,在此无法详述;但本文中的代码可以提供一个出发点,让您去开发符合自己特定需求的分析工具。

代理初始化

本节介绍用于初始化代理的代码。首先,代理必须包括 jvmti.h 文件,语句为 #include

另外,代理必须包含一个名为 agent_onload 的函数,加载库时要调用这一函数。agent_onload 函数用于在初始化 java virtual machine (jvm) 之前设置所需的功能。agent_onload 签名如下所示:

jniexport jint jnicall agent_onload(javavm *jvm, char *options, void *reserved) {

...

/* we return jni_ok to signify success */

return jni_ok;

}

在我们的示例代码中,我们必须为将要使用的 jvmti 函数和事件启用多种功能。一般情况下均需(在某些情况下必须)将这些功能添加到 agent_onload 函数中。有关每种函数或事件所需的功能的说明,参见 java 虚拟机工具接口页。例如,要使用 interruptthread 函数,can_signal_thread 功能必须为 true。我们把示例所需的全部功能都设置为 true,然后使用 addcapabilities 函数将它们添加到 jvmti 环境中:

static jvmtienv *jvmti = null;

static jvmticapabilities capa;

jvmtierror error;

...

(void)memset(&capa, 0, sizeof(jvmticapabilities));

capa.can_signal_thread = 1;

capa.can_get_owned_monitor_info = 1;

capa.can_generate_method_entry_events = 1;

capa.can_generate_exception_events = 1;

capa.can_generate_vm_object_alloc_events = 1;

capa.can_tag_objects = 1;

error = (*jvmti)->addcapabilities(jvmti, &capa);

check_jvmti_error(jvmti, error, "unable to get necessary jvmti capabilities.");

...

此外,agent_onload 函数通常用于注册事件通知。在此示例中,我们在使用 seteventnotificationmode 函数的 agent_onload 中启用了多个事件,如 vm initialization event、vm death event 和 vm object allocation, 如下所示:

error = (*jvmti)->seteventnotificationmode

(jvmti, jvmti_enable, jvmti_event_vm_init, (jthread)null);

error = (*jvmti)->seteventnotificationmode

(jvmti, jvmti_enable, jvmti_event_vm_death, (jthread)null);

error = (*jvmti)->seteventnotificationmode

(jvmti, jvmti_enable, jvmti_event_vm_object_alloc, (jthread)null);

check_jvmti_error(jvmti, error, "cannot set event notification");

...

注意,在此示例中,null 是作为第三个参数传递的,它可以全局地启用事件通知。如果需要,可以为某个特殊线程启用或禁用某些事件。

我们为其注册的每个事件还都必须具有一个指定的回调函数,当该事件发生时将调用它。例如,如果一个 exception 类型的 jvmti event 发生,示例代理会将其发送到回调方法 callbackexception() 中。

使用 jvmtieventcallbacks 结构和 seteventcallbacks 函数可以完成此任务:

jvmtieventcallbacks callbacks;

...

(void)memset(&callbacks, 0, sizeof(callbacks));

callbacks.vminit = &callbackvminit; /* jvmti_event_vm_init */

callbacks.vmdeath = &callbackvmdeath; /* jvmti_event_vm_death */

callbacks.exception = &callbackexception;/* jvmti_event_exception */

callbacks.vmobjectalloc = &callbackvmobjectalloc;/* jvmti_event_vm_object_alloc */

error = (*jvmti)->seteventcallbacks(jvmti, &callbacks,(jint)sizeof(callbacks));

check_jvmti_error(jvmti, error, "cannot set jvmti callbacks");

...

我们还将设置一个全局代理数据区域以在整个代码中使用。

/* global agent data structure */

typedef struct {

/* jvmti environment */

jvmtienv *jvmti;

jboolean vm_is_started;

/* data access lock */

jrawmonitorid lock;

} globalagentdata;

static globalagentdata *gdata;

在 agent_onload 函数中,我们执行以下设置:

/* setup initial global agent data area

* use of static/extern data should be handled carefully here.

* we need to make sure that we are able to cleanup after

* ourselves so anything allocated in this library needs to be

* freed in the agent_onunload() function.

*/

static globalagentdata data;

(void)memset((void*)&data, 0, sizeof(data));

gdata = &data;

...

/* here we save the jvmtienv* for agent_onunload(). */

gdata->jvmti = jvmti;

...

我们在 agent_onload() 中创建一个原始监视器,然后把代码 vm_init、vm_death 和 exception 包装于 jvmti rawmonitorenter() 和 rawmonitorexit() 接口 。

/* here we create a raw monitor for our use in this agent to

* protect critical sections of code.

*/

error = (*jvmti)->createrawmonitor(jvmti, "agent data", &(gdata->lock));

/* enter a critical section by doing a jvmti raw monitor enter */

static void

enter_critical_section(jvmtienv *jvmti)

{

jvmtierror error;

error = (*jvmti)->rawmonitorenter(jvmti, gdata->lock);

check_jvmti_error(jvmti, error, "cannot enter with raw monitor");

}

/* exit a critical section by doing a jvmti raw monitor exit */

static void

exit_critical_section(jvmtienv *jvmti)

{

jvmtierror error;

error = (*jvmti)->rawmonitorexit(jvmti, gdata->lock);

check_jvmti_error(jvmti, error, "cannot exit with raw monitor");

}

卸载代理时,vm 将调用 agent_onunload。此函数用于清理在 agent_onload 期间分配的资源。

/* agent_onunload: this is called immediately before the shared library

* is unloaded. this is the last code executed.

*/

jniexport void jnicall agent_onunload(javavm *vm)

{

/* make sure all malloc/calloc/strdup space is freed */

}

使用 jvmti 分析线程

本节介绍如何获取关于在 jvm 中运行的用户线程的信息。如前所述,启动 jvm 时,jvmti 代理库中的启动函数 agent_onload 将被调用。在 vm 初始化过程中,jvmti_event_vm_init 类型的 jvmti event 将生成并被发送到代理代码的 callbackvminit 例程中。一旦 vm 初始化事件被接收(即 调用vminit 回调),代理即可结束其初始化。现在,此代理可以自由调用任何 java native interface (jni) 或 jvmti 函数。此时,我们已经处于活动阶段,将启用本 vminit 回调例程中的 exception 事件(jvmti_event_exception)。

error = (*jvmti)->seteventnotificationmode

(jvmti, jvmti_enable, jvmti_event_exception, (jthread)null);

无论何时,只要在 java 编程语言方法中首次探测到异常,就会生成 exception 事件。此异常可能由 java 编程语言抛出,也可能由本地方法抛出;但是如果由本地方法抛出,直到 java 编程语言方法首次发现此异常时该事件才会生成。如果异常已被处理并清除,则异常事件不会生成。

出于演示目的,下面给出了所用的示例 java 应用程序。主线程创建了 5 个线程,这 5 个线程退出前各自抛出一个异常。一旦启动 jvm,jvmti_event_vm_init 将生成并被发送到代理代码中进行处理,因为我们已经在代理代码中启用了 vminit 和 exception 事件。随后,当 java 线程抛出一个异常时,jvmti_event_exception 将被发送到代理代码中。然后,代理代码 会分析此线程信息并显示当前线程名、它所属的线程组、此线程所拥有的监视器、线程状态、线程堆栈跟踪及 jvm 中的所有用户线程。

public class simplethread {

static mythread t;

public static void main(string args[]) throws throwable{

t = new mythread();

system.out.println("creating and running 10 threads...");

for(int i = 0; i < 5; i++) {

thread thr = new thread(t,"mythread"+i);

thr.start();

try {

thr.join();

} catch (throwable t) {

}

}

}

}

class mythread implements runnable {

thread t;

public mythread() {

}

public void run() {

/* no-op */

try {

"a".getbytes("ascii");

throwexception();

thread.slee

jvmti 异常事件生成后将被发送到代理代码的 exception 回调例程中。代理必须添加 can_generate_exception_events 功能才能启用异常事件。我们使用 jvmti getmethodname 接口来显示生成异常的方法名和例程签名。

err3 = (*jvmti)->getmethodname(jvmti, method, &name, &sig, &gsig);

printf("exception in method:%s%s\n", name, sig);

我们使用 jvmti getthreadinfo 和 getthreadgroupinfo 接口来显示当前线程和组详细信息。

err = (*jvmti)->getthreadinfo(jvmti, thr, &info);

if (err == jvmti_error_none) {

err1 = (*jvmti)->getthreadgroupinfo(jvmti,info.thread_group, &groupinfo);

...

if ((err == jvmti_error_none) && (err1 == jvmti_error_none ))

{

printf("got exception event, current thread is : %s and thread group is: %s\n",

((info.name==null) ? ""

: info.name), groupinfo.name);

}

}

这将在您的终端上产生以下输出:

got exception event, current thread is : mythread0 and thread group is: main

使用 jvmti getownedmonitorinfo 接口可以获取关于指定线程所拥有的监视器的信息。此函数 不要求挂起线程。

err = (*jvmti)->getownedmonitorinfo(jvmti, thr, νm_monitors, &arr_monitors);

printf("number of monitors returned : %d\n", num_monitors);

使用 jvmti getthreadstate 接口可以获取线程的状态信息。

线程状态可以为以下值之一:

线程已终止

线程活动

线程可运行

线程休眠

线程在等待通知

线程处于对象等待状态

线程为本地状态

线程已挂起

线程已中断

err = (*jvmti)->getthreadstate(jvmti, thr, &thr_st_ptr);

if ( thr_st_ptr & jvmti_thread_state_runnable ) {

printf("thread: %s is runnable\n", ((info.name==null) ? "" : info.name));

flag = 1;

}

使用 jvmti 显示 jvm 中的所有用户线程

jvmti 函数 getallthreads 用于显示 jvm 已知的所有活动线程。这些线程是关联到 vm 的 java 编程语言线程。

以下代码对此进行了说明:

/* get all threads */

err = (*jvmti)->getallthreads(jvmti, &thr_count, &thr_ptr);

if (err != jvmti_error_none) {

printf("(getallthreads) error expected: %d, got: %d\n", jvmti_error_none, err);

describe(err);

printf("\n");

}

if (err == jvmti_error_none && thr_count >= 1) {

int i = 0;

printf("thread count: %d\n", thr_count);

for ( i=0; i < thr_count; i++) {

/* make sure the stack variables are garbage free */

(void)memset(&info1,0, sizeof(info1));

err1 = (*jvmti)->getthreadinfo(jvmti, thr_ptr[i], &info1);

if (err1 != jvmti_error_none) {

printf("(getthreadinfo) error expected: %d, got: %d\n", jvmti_error_none, err1);

describe(err1);

printf("\n");

}

printf("running thread#%d: %s, priority: %d, context class loader:%s\n", i+1,info1.name,

info1.priority,(info1.context_class_loader == null ? ": null" : "not null"));

/* every string allocated by jvmti needs to be freed */

err2 = (*jvmti)->deallocate(jvmti, (void*)info1.name);

if (err2 != jvmti_error_none) {

printf("(getthreadinfo) error expected: %d, got: %d\n", jvmti_error_none, err2);

describe(err2);

printf("\n");

}

}

}

这将在您的终端上产生以下输出:

thread count: 5

running thread#1: mythread4, priority: 5, context class loader:not null

running thread#2: signal dispatcher, priority: 10, context class loader:not null

running thread#3: finalizer, priority: 8, context class loader:: null

running thread#4: reference handler, priority: 10, context class loader:: null

running thread#5: main, priority: 5, context class loader:not null

获取 jvm 线程堆栈跟踪

jvmti 接口 getstacktrace 可用于获取关于线程堆栈的信息。如果 max_count 小于堆栈的深度,最深框架的 max_count 数将返回,否则返回整个堆栈。调用此函数无需挂起线程。

下例产生至多 5 个最深框架。如果存在任何框架,则还将输出当前执行的方法名。

/* get stack trace */

err = (*jvmti)->getstacktrace(jvmti, thr, 0, 5, &frames, &count);

if (err != jvmti_error_none) {

printf("(getthreadinfo) error expected: %d, got: %d\n", jvmti_error_none, err);

describe(err);

printf("\n");

}

printf("number of records filled: %d\n", count);

if (err == jvmti_error_none && count >=1) {

char *methodname;

methodname = "yet_to_call()";

char *declaringclassname;

jclass declaring_class;

int i=0;

printf("exception stack trace\n");

printf("=====================\n");

printf("stack trace depth: %d\n", count);

for ( i=0; i < count; i++) {

err = (*jvmti)->getmethodname

(jvmti, frames[i].method, &methodname, null, null);

if (err == jvmti_error_none) {

err = (*jvmti)->getmethoddeclaringclass(jvmti, frames[i].method, &declaring_class);

err = (*jvmti)->getclasssignature(jvmti, declaring_class, &declaringclassname, null);

if (err == jvmti_error_none) {

printf("at method %s() in class %s\n", methodname, declaringclassname);

}

}

}

}

这将使您的终端产生以下输出:

number of records filled: 3

thread stack trace

=====================

stack trace depth: 3

at method throwexception() in class lmythread;

at method run() in class lmythread;

at method run() in class ljava/lang/thread;

使用 jvmti 分析堆

本节介绍如何获取关于使用堆的信息的示例代码。例如,我们已经按“代理初始化”一节中所述为 vm object allocation 事件进行了注册。当 jvm 分配了 java 编程语言可见但其他工具机制不能探测到的对象时,我们将得到通知。这一点与 jvmpi 截然不同,jvmpi 在分配任何对象时都将发送事件。在 jvmti 中,针对用户分配的对象不会发送任何事件,因为它期望使用的是字节码工具。例如,在 simplethread.java 程序中,分配 mythread 或 thread 对象时,我们是不会得到通知的。以后将单独发表一篇文章,描写如何使用字节码工具获取此信息。

vm object allocation 事件对于确定有关由 jvm 分配的对象的信息十分有用。在 agent_onload 方法中,我们将 callbackvmobjectalloc 注册为发送 vm object allocation 事件时调用的函数。回调函数参数包含关于已分配对象的信息,如对象类和对象大小的 jni 本地参考。借助于 jclass 参数 object_klass,我们可以使用 getclasssignature 函数获取关于类名的信息。我们可以把下面给出的对象类及其大小打印出来。注意避免过多的输出,我们仅需输出超过 50 个字节的对象信息就行了。

/* callback function for vm object allocation events */

static void jnicall callbackvmobjectalloc

(jvmtienv *jvmti_env, jnienv* jni_env, jthread thread,

jobject object, jclass object_klass, jlong size) {

...

char *classname;

...

if (size > 50) {

err = (*jvmti)->getclasssignature(jvmti, object_klass, &classname, null);

if (classname != null) {

printf("\ntype %s object allocated with size %d\n", classname, (jint)size);

}

...

我们使用上面所介绍的 getstacktrace 方法来输出正在分配该对象的线程的堆栈跟踪。我们依照该节所述获取指定深度的 框架。这些框架将作为 jvmtiframeinfo 结构返回,这些结构包含每个框架的 jmethodid(即 frames[x].method)。getmethodname 函数可以将 jmethodid 映射到特殊的方法名中。在此示例的最后部分,我们还将使用 getmethoddeclaringclass 和 getclasssignature 函数获取从其中调用过此方法的类的名称。

char *methodname;

char *declaringclassname;

jclass declaring_class;

jvmtierror err;

//print stack trace

jvmtiframeinfo frames[5];

jint count;

int i;

err = (*jvmti)->getstacktrace(jvmti, null, 0, 5, &frames, &count);

if (err == jvmti_error_none && count >= 1) {

for (i = 0; i < count; i++) {

err = (*jvmti)->getmethodname(jvmti, frames[i].method, &methodname, null, null);

if (err == jvmti_error_none) {

err = (*jvmti)->getmethoddeclaringclass(jvmti, frames[i].method, &declaring_class);

err = (*jvmti)->getclasssignature(jvmti, declaring_class, &declaringclassname, null);

if (err == jvmti_error_none) {

printf("at method %s in class %s\n", methodname, declaringclassname);

}

}

}

}

...

注意,完成任务时应释放由这些函数分配给 char 数组的内存:

err = (*jvmti)->deallocate(jvmti, (void*)classname);

err = (*jvmti)->deallocate(jvmti, (void*)methodname);

err = (*jvmti)->deallocate(jvmti, (void*)declaringclassname);

...

此代码的输出如下所示:

type ljava/lang/reflect/constructor; object allocated with size 64

at method getdeclaredconstructors0 in class ljava/lang/class;

at method privategetdeclaredconstructors in class ljava/lang/class;

at method getconstructor0 in class ljava/lang/class;

at method getdeclaredconstructor in class ljava/lang/class;

at method run in class ljava/util/zip/zipfile$1;

原始类的返回名称是相应原始类型的签名字符类型。例如,java.lang.integer.type 为“i”。

在 vm object allocation 的回调方法中,我们仍将使用 iterateoverobjectsreachablefromobject 函数演示如何获取关于堆的附加信息。在此示例中,我们将 jni 参考作为一个参数传递给刚刚分配的对象,该函数将在此新分配对象所能直接或间接到达的所有对象中迭代。对于每个可到达的对象,另外还有一个定义的回调函数可对其进行描述。在此示例中,传递到 iterateoverobjectsreachablefromobject 的回调函数名为 reference_object:

err = (*jvmti)->iterateoverobjectsreachablefromobject

(jvmti, object, &reference_object, null);

if ( err != jvmti_error_none ) {

printf("cannot iterate over reachable objects\n");

}

...

reference_object 函数定义如下:

/* jvmti callback function. */

static jvmtiiterationcontrol jnicall

reference_object(jvmtiobjectreferencekind reference_kind,

jlong class_tag, jlong size, jlong* tag_ptr,

jlong referrer_tag, jint referrer_index, void *user_data)

{

...

return jvmti_iteration_continue;

}

...

在此示例中,我们使用 iterateoverobjectsreachablefromobject 函数计算新分配对象所能到达的所有对象的 总的大小,以及它们的对象类型。对象类型可以从 reference_kind 参数中确定。然后打印此信息以接收如下输出:

this object has references to objects of combined size 21232

this includes 45 classes, 9 fields, 1 arrays, 0 classloaders, 0 signers arrays,

0 protection domains, 19 interfaces, 13 static fields, and 2 constant pools.

注意,位于 jvmti 中的类似迭代函数允许迭代的对象有:整个堆(可到达的和不可到达的);根目录对象和根目录对象所能直接或间接到达的所有对象;堆中是指定类的实例的所有对象。使用这些函数的技巧和前面所介绍的类似。在执行这些函数期间,堆的状态没有任何变化:没有分配任何对象,没有对任何对象进行垃圾收集,并且对象的状态(包括堆值)也没有任何变化。结果,执行 java 编程语言代码的线程、尝试恢复执行 java 编程语言代码的线程和尝试执行 jni 函数的线程都完全停了下来。所以,在对象参考回调函数中,不能使用任何 jni 函数;在没有特别允许的情况下,也不允许使用任何 jvmti 函数。

编译和执行示例代码

要编译并运行这里描述的示例应用程序的代码,请按以下步骤操作:

设置 jdk_path 为指向 j2se 1.5 发行版

jdk_path="/home/xyz/j2sdk1.5.0/bin"

使用 c 语言编译器构建共享库。我们使用的是 sun studio 8 c 编译器。

cc="/net/compilers/s1studio_8.0/sunwspro/bin/cc"

echo "...creating liba.so"

${cc} -g -kpic -o liba.so

-i${jdk_path}/include -i${jdk_path}/include/solaris a.c

要加载并运行代理库,请在 vm 启动过程中使用下面的命令行参数之一。

-agentlib:

-agentpath:/home/foo/jvmti/

然后如下运行示例 java 应用程序:

echo "...creating simplethread.class"

${jdk_path}/bin/javac -g -d . simplethread.java

echo "...running simplethread.class"

ld_library_path=. classpath=. ${jdk_path}/bin/java

-showversion -agentlib:a simplethread

注意:此示例代理代码是在 solaris 9 operating system 上构建和测试的。

结束语

在本文中,我们演示了 jvmti 提供用于监控和管理 jvm 的一些接口。jvmti 规范 (jsr-163) 旨在为需要访问 vm 状态的广泛的工具提供一个 vm 接口,这些工具包括但不限于:分析、调试、监控、线程分析和覆盖率分析工具。

建议开发人员不要使用 jvmpi 接口开发工具或调试实用工具,因为 jvmpi 是一种不受支持的实验技术。应考虑使用 jvmti 编写 java 虚拟机的所有分析和管理工具。

扫描关注微信公众号