概述
JVMTI是JDK提供的一套用于开发JVM监控,问题定位与性能调优工具的通用编程接口(API)。
通过JVMTI,我们可以开发各式各样的JVMTIAgent。这个Agent的表现形式是一个以c/c++语言编写的动态共享库。
JVMTIAgent原理:java启动或运行时,动态加载一个外部基于JVMTI编写的dynamicmodule到Java进程内,然后触发JVM源生线程AttachListener来执行这个dynamicmodule的回调函数。在函数体内,你可以获取各种各样的VM级信息,注册感兴趣的VM事件,甚至控制VM的行为。
JVMTI从功能上大致可以分为4类:
1.Heap
获取所有类的信息,对象信息,对象引用关系,FullGC开始/结束,对象回收事件等。
2.线程与堆栈
获取所有线程的信息,线程组信息,控制线程(start,suspend,resume,interrupt…),ThreadMonitor(Lock),得到线程堆栈,控制出栈,方法强制返回,方法栈本地变量等。
3.Class&Object&Method&Field元信息
class信息,符号表,方法表,redefineclass(hotswap),retransformclass,object信息,fields信息,method信息等。
4.工具类
线程cpu消耗,classloader路径修改,系统属性获取等。
开发jvmtiagent,简单的来讲,就是开发一个c/c++的共享库。在windows下后缀是dll,linux/unix下是so,mac下就是dylib。所以我们创建工程和编译环境的时候,记得以共享库(sharelibrary)的形式来构建。
JVMTI的启动方式
JVMTI有两种启动方式,第一种是随java进程启动时,自动载入共享库,下文简称方式A。另一种方式是,java运行时,通过attachapi动态载入,下文简称方式B。
方式A的实现方式是通过在java启动时传递一个特殊的option,例子如下:
java-agentlib:<agent-lib-name>=<options>Sample
注意,这里的共享库路径是环境变量路径,例如java-agentlib:foo=opt1,opt2,java启动时会从linux的LD_LIBRARY_PATH或windows的PATH环境变量定义的路径处装载foo.so或foo.dll,找不到则抛异常
java-agentpath:<path-to-agent>=<options>Sample
这是以绝对路径的方式装载共享库,例如java-agentpath:/home/admin/agentlib/foo.so=opt1,opt2
方式B的实现方式是通过attachapi,这是一套纯java的api,它负责动态地将dynamicmoduleattach到指定进程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(); } }
AttachAPI位于$JAVA_HOME/lib/tools.jar,所以在编译时,需要将这个jar放入classpath。例如
javac-cp$JAVA_HOME/lib/tools.jarVMAttacher.java
准备开发JVMTIAgent
1.jvmti头文件搜索路径
c/c++应用程序都会用到头文件,为了让共享库编译时能找到jvmti的头文件,我们需要添加2个路径到Includes。
一个指向到$JAVA_HOME/include,另一个指向到$JAVA_HOME/include/linux,windows下为$JAVA_HOME/include/windows
例如:
g++-I/home/kenwu/jdk1.6.0_24/include-I/home/kenwu/jdk1.6.0_24/include/linux
上面是以命令行方式编译,如果你使用的是IDE,请在相应位置添加Includes.
2.源码引入jvmti.h头文件
jvmti.h头文件里包含了所有jvmti要用到的数据结构和回调函数定义。
#include<jvmti.h>
3.确定JVMTI的启动方式,引入回调函数
方式A引入的回调函数如下:
JNIEXPORTjintJNICALL
Agent_OnLoad(JavaVM*vm,char*options,void*reserved)
方式B引入的回调函数如下:
JNIEXPORTjintJNICALL
Agent_OnAttach(JavaVM*jvm,char*options,void*reserved){
卸载共享库时的回调函数(通用):
JNIEXPORTvoidJNICALLAgent_OnUnload(JavaVM*vm)
我们可以看到,方式A和方式B提供的回调函数,参数是一样的。
三个参数分别是jvm,options,reserved.
jvm变量是JVM传递给共享库的上下文。通过jvm变量,我们可以创建jvmti环境上下文。需要注意的是,jvmti上下文不同于jvm上下文,这从jvm变量类型定义的函数上可以看出。
options是外部传入的参数。比如前面例子里给的opt1,opt2,它仅仅是一个字符串。
reserved,这是一个预留参数,我们不必关心它。
开发一个简单的JVMTIAgent
下面,给大家演示开发一个的简单Agent.
“ Agent代码: /* * Just a simple agent using JVMTI. * * Created on: 2011-3-3 * Author: kenwu */ #include <jvmti.h> #include <string> #include <cstring> #include <iostream> #include <list> #include <map> #include <set> #include <stdlib.h> #include <jni_md.h> JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *jvm, char *options, void *reserved) { jvmtiEnv *jvmti; jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1); if (result != JNI_OK) { printf(“ERROR: Unable to access JVMTI!n“); } jvmtiError err = (jvmtiError) 0; jclass *classes; jint count; err = jvmti->GetLoadedClasses(&count, &classes); if (err) { printf(“ERROR: JVMTI GetLoadedClasses failed!n“); } for (int i = 0; i < count; i++) { char *sig; jvmti->GetClassSignature(classes[i], &sig, NULL); printf(“cls sig=%sn“, sig); } return err; } JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) { // nothing to do }
功能是打印jvm内所有已经装载成功的class签名。
完成Agent源码的编写后,我们来编译它。
BASE_HOME=`pwd`
INCLUDES=”-I$JAVA_HOME/include-I$JAVA_HOME/include/linux”
g++$BASE_HOME/src/agent.cpp$INCLUDES-Wall-Wno-deprecated-fPIC�Cshare-o$BASE_HOME/libtestagent.so
例子中的BASE_HOME为笔者的工程根目录,请自行替换。
编译完后,我们需要准备一个java进程的Attach工具类。最简单的实现方式,就是直接写一个带main的javaapplication。
笔者将上文中提到的VMAttacher例子稍做了一下修改:
importjava.io.IOException;
importcom.sun.tools.attach.VirtualMachine;
publicclassTestAgentVMAttacher{
publicstaticvoidmain(String[]args)throwsException{
Stringpid=“12345″;//12345改成你想attach的java进程id
StringagentPath=“/path_to_agent”;//path_to_agent为你编译的agent的路径
VirtualMachinevirtualMachine=com.sun.tools.attach.VirtualMachine.attach(pid);
virtualMachine.loadAgentPath(agentPath,null);
virtualMachine.detach();
}
}
最后,运行这个javaapplication。如果你的console能看到如下输出,说明agentload并运行成功:
“ cls sig=Lsun/jkernel/DownloadManager$1; cls sig=Lsun/nio/cs/StandardCharsets$Cache; cls sig=Lsun/misc/URLClassPath; cls sig=Ljava/nio/HeapCharBuffer; cls sig=Lsun/nio/ByteBuffered; cls sig=Ljava/lang/StringCoding$StringDecoder; cls sig=Ljava/lang/reflect/Modifier; cls sig=Ljava/util/Collections; ….
PS:灵活使用AttachAPI,还可以实现更多高级的功能,在本文中不再赘述。
总结
通过本文的学习,你了解了JVMTI,学会了如何配置JVMTI开发环境,并在自己搭建的环境中开发了一个简单的打印所有已装载class签名的agent。
在下一节中,我们将深入到JVMTIHeap相关的各个函数,利用这些函数实现一个多功能的堆内存分析程序。