The JVM Tool Interface (JVMTI) 是一个由JVM提供的用于开发针对Java程序开发与监控工具的编程接口,通过JVMTI接口(Native API)可以创建代理程序(Agent)以监视和控制 Java 应用程序,包括剖析、调试、监控、分析线程等。著名的JProfiler利用该项技术实现其对Java程序的运行态监控与性能分析。
值得注意的是JVMTI 并不一定在所有的 Java 虚拟机上都得到实现,目前Oracle(SUN)、IBM、OpenJDK以及一些开源的如 Apache Harmony DRLVM均对其进行了标准实现 。
由于JVMTI 是一套Native接口,因此使用 JVMTI 需要我们使用C/C++ 操纵JNI。
JVMTI程序通常通过Agent方式在JVM OnLoad phase(启动时)Start-Up,这个加载处于虚拟机初始化的早期,此时所有的 Java 类都未被初始化、所有的对象实例也都未被创建(也支持Live phase(运行时)的Start-Up)。在启动Java应用程序时,需加入以下JVM参数:
-agentlib:agent-lib-name=options
-agentpath:path-to-agent=options
JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会主动调用一些事件的回调接口,这些接口可以供开发者扩展自己的逻辑,实际上,对于JVMTI程序的Load过程可以遵循一种模板式的流程框架来完成:
(1)获取JVMTI环境(JVMTI Environment)
(2)注册所需的功能(Capabilities)
(3)注册事件通知(Event Notification)
(4)指定事件回调函数(Callback Method)
可以通过http://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html 和https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/ 进一步了解相关知识。
接下来,我们通过举例的方式,看看JVMTI能够为Java应用监测带来些什么?
测试程序
我们首先编写一个简单的测试程序,用于展示我们举例中JVMTI Agent程序的功能,程序清单参考如下:
(1)Foo类
package org.xreztento.tester;
public class Foo {
public void bar() throws InterruptedException {
Thread.sleep(500);
System.out.println("Executing Foo.bar()");
}
public void baz() {
System.out.println("Executing Foo.baz()");
}
}
(2)Main类
package org.xreztento.tester;
public class Main {
public static void main(String[] args) throws InterruptedException{
Thread[] threads = new Thread[5];
Foo foo = new Foo();
foo.bar();
foo.baz();
for(int i = 0; i < threads.length; i++){
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
for(Thread thread : threads){
thread.start();
thread.join();
}
}
}
我们将项目打包为tester.jar包,运行后输出结果如下:
Bytecode Instrumentation
使用 Instrumentation开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。
利用Instrumentation实现字节码增强是许多监控工具针对Java应用程序实现非“侵入式”监控技术的基础,JVMTI为其提供了Native接口,Java SE 5将其从本地代码中解放出来通过JavaAgent利用该本地接口实现了Java语言层级的接口。
我们这里先不讨论JavaAgent的上层实现方式,你可以直接利用JVMTI的Native接口完成class字节码加载时的字节码修改增强。在JVM加载class字节码时会产生一个JVMTI_EVENT_CLASS_FILE_LOAD_HOOK事件,你可以通过ClassFileLoadHook回调函数完成新字节码的定义工作。
需要特别注意的地方是,对字节码的修改需要开辟出一块新的内存空间,因此就像向操作系统申请内存空间使用如malloc一样,你需要使用(*jvmti)->Allocate在JVM内部申请出一块内存空间,参考如下代码:
#include <stdio.h>
#include <memory.h>
#include <string.h>
#include <jvmti.h>
void JNICALL callbackClassFileLoadHook(jvmtiEnv *jvmti,
JNIEnv *jni,
jclass class_being_redefined,
jobject loader,
const char *name,
jobject protection_domain,
jint class_data_len,
const unsigned char *class_data,
jint *new_class_data_len,
unsigned char **new_class_data) {
jvmtiError error;
if(strcmp(name, "org/xreztento/tester/Foo") == 0){
printf("loaded class name=%s\n ", name);
jint size = class_data_len;
*new_class_data_len = size;
//为新的class字节码数据区分配JVM内存
error = (*jvmti)->Allocate(jvmti, size, new_class_data);
memset(*new_class_data, 0, size);
if(error != JVMTI_ERROR_NONE) {
fprintf(stderr, "ERROR: Unable to AddCapabilities JVMTI");
}
int i;
//遍历旧的字节码字符,将E字符修改为P
for(i = 0; i < size; i++){
if(class_data[i] == 'E'){
(*new_class_data)[i] = 'P';
} else {
(*new_class_data)[i] = class_data[i];
}
}
}
}
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM* vm, char *options, void *reserved){
jvmtiEnv *jvmti = NULL;
jvmtiError error;
//获取JVMTI environment
error = (*vm)->GetEnv(vm, (void **)&jvmti, JVMTI_VERSION_1_1);
if (error != JNI_OK) {
fprintf(stderr, "ERROR: Couldn't get JVMTI environment");
return JNI_ERR;
}
//注册功能
jvmtiCapabilities capabilities;
(void)memset(&capabilities, 0, sizeof(jvmtiCapabilities));
capabilities.can_generate_all_class_hook_events = 1 ;
capabilities.can_retransform_classes = 1 ;
capabilities.can_retransform_any_class = 1 ;
error = (*jvmti)->AddCapabilities(jvmti, &capabilities);
if(error != JVMTI_ERROR_NONE) {
fprintf(stderr, "ERROR: Unable to AddCapabilities JVMTI");
return error;
}
//设置JVM事件回调
jvmtiEventCallbacks callbacks;
callbacks.ClassFileLoadHook = &callbackClassFileLoadHook;
error = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, (jint)sizeof(callbacks));
if(error != JVMTI_ERROR_NONE) {
fprintf(stderr, "ERROR: Unable to SetEventCallbacks JVMTI!");
return error;
}
//设置事件通知
error =