天天看点

使用JVMTI获取Java多线程程序指令执行次序 使用JVMTI获取Java多线程程序指令执行次序

在java多线程程序中,由于线程调度,指令间的次序在每次运行时都可能不相同,有时候,我们需要得到指令次序,用来分析程序的行为。这样细粒度的底层行为用一般方法很难完成,我们需要借助 ,即jvmti,来帮助我们获取java虚拟机执行时的信息。本文先介绍编写jvmti程序的基本框架,然后介绍如何使用jvmti来获取多线程程序中指令之间的次序。

jvmti是用于编写开发与监视工具的编程接口,使用它可以检查并控制运行于java虚拟机上的程序。使用它可以完成性能分析,调试,监视(monitoring),线程分析,覆盖分析(coverage analysis)等工具。

使用jvmti可以编写出一个agent。在运行java程序时,指定这个agent,那么当虚拟机运行程序时,如果agent中指定的一些事件发生,虚拟机就会调用agent中相应的回调函数。jvmti提供了一系列可以指定的事件,以及获取虚拟机中信息的函数接口。

头文件 agent程序中,需要包含 <code>jvmti.h</code>头文件,才能使用jvmti中提供的接口。

基本事件 和agent有关的两个基本事件是agent的启动与关闭,我们需要自己编写与启动与关闭相关的函数,这样,虚拟机才知道启动与关闭agent时,都需要做些什么。

与启动相关的函数有两个,如果你的agent在虚拟机处于<code>onload</code>阶段时启动,会调用<code>agent_onload</code>函数,如果你的agent在虚拟机处于<code>live</code>阶段时启动,会调用<code>agent_onattach</code>函数。

我的理解是,如果你的agent想要全程监视一个程序的运行,就编写<code>agent_onload</code>,并在启动虚拟机时指定agent。如果你的agent想获取一个已经在运行的虚拟机中程序的信息,就编写<code>agent_onattach</code>。

两个函数的原型如下:

与agent关闭相关的函数是<code>agent_onunload</code>,当agent要被关闭时,虚拟机会调用这个函数,函数原型为:

程序基本框架

主要的内容框架在<code>agent_onload</code>中编写:

1 获取jvm环境

可以为同一个虚拟机指定多个agent,每个agent都有自己的环境,在指定agent行为前,首先要获取的就是环境信息,后面的操作都是针对这个环境的。另外,jvmti中的函数都会返回错误代码,在调用函数后,需要检查返回值,以确定函数调用是否成功。不同的函数会返回不同类型的错误码,可自行参阅jvmti的api。

另外,需要注意,jvmti程序可以使用c/c++编写,两者在调用函数时略有不同,上面的例子是用c编写,gcc编译。如果你使用c++编写,<code>getenv</code>需要这样调用:

其它函数依次类推。

2 添加capabilities

jvmti中有很多事件,每个事件都对对应一些capabilities,如果你想为此事件编写函数,就要开启相应的capabilities,例如,我们想对 <code>jvmti_event_single_step</code> 事件编写函数,可以查到,需要开启<code>can_generate_single_step_events</code>:

如果开启的capabilities多于一个,不用声明多个<code>jvmticapabilities</code>变量,只需要使用类似

的方式指定就行。

3 指定事件

jvmti编写的目的是,当虚拟机中一个事件发生时,调用我们为此事件编写的函数。所以我们需要指定哪个事件发生时,通知agent:

其中 <code>jvmti_event_single_step</code> 就是事件代码。

需要特别注意的是,要先开启相关capabilities,然后才能指定事件。

4 设置回调函数

我们还需要为事件指定回调函数,并自行编写回调函数,事件回调函数的接口是由jvmti指定的,例如<code>jvmti_event_single_step</code>事件的回调函数原型:

为事件指定回调函数的方法是:

之后,我们需要自己编写 <code>callbacksinglestep</code>函数:

运行agent,通过指定虚拟机参数来设定,例如运行<code>possiblereordering</code>时:

其中<code>traceagent.so</code>就是编译后生成的agent。

我们知道,在java虚拟机中的运行时数据区中,每个线程都有它的私有区域,每个线程有自己的pc寄存器,pc寄存器表示线程当前执行的指令在内存中的地址。其实我最初的目的是想得到这个pc的值,但是找了很久都没有找到,然后在jvmti中找到了类似的概念。

在jvmti中,介绍单步事件(single step event)时说,当一个线程到达一个新的位置(location)时,单步事件就会产生。单步事件使agent以虚拟机允许的最细粒度,跟踪线程执行。

我们回到单步事件回调函数的原型:

其中的 <code>location</code> 就是新指令的位置。

我们首先来编写一个java多线程程序,这个程序是 《java并发编程实战》(java concurrency in practice) 中的一个例子,我做了一点变形:

给thread other多加了一些语句,用以区分两个线程。

这里有一个问题是,我们关心的其实只是两个线程的 <code>run</code>函数中指令的次序,而单步事件会在任何指令执行时,都调用回调函数,这就需要我们在回调函数中,只保留源代码中的两个线程的<code>run</code>函数中的指令的位置,其它的都过滤掉。

我们可以使用jvmti提供的 <code>getmethodname</code> 来得到函数名,使用 <code>getmethoddeclaringclass</code>得到类名,然后通过比较类名和函数名,只保留 <code>run</code>中的指令:

执行下列命令:

即可得到指令次序信息:

最终的源代码中,我还输出了线程名,和方法的指令地址范围。

我们可以反编译 <code>possiblereordering$1</code> 和 <code>possiblereordering$2</code>,看看相应的指令范围是否可以对应上。

可以看出,确实是一个线程的run方法指令范围是 <code>0:10</code> ,另一个是 <code>0:80</code>,说明我们正确获取了相应指令。

完整的源代码,包含如何编译,运行,可以在我的github中找到:

扩展阅读:

1. 

2.

3. :下载后可在目录 jdk1.8.0_05/demo/jvmti/ 下找到多个demo