分享

第一次尝试jvmti - Agile - 博客大巴

 木木的阳光 2011-04-10

JVMTI是JDK1.5众多最新特性的一项,通过JVMTI我们可以做到许多以前难以做
到的事情.很多时候,最近我对程序建立了多少个对象以及每个对象的大小很感兴
趣.看了一些资料,知道可以通过代码注入来实现,也就是向java.lang.Object的构造
方法中注入代码,类似下面的代码:
public Object(){
       if(HeapTracker.engaged>0){
 HeapTracker.newobj(this);
       }
}

而Trace的代码类似下面的代码:
public class HeapTracker {
 
    private static int engaged = 0;
 
    private static native void _newobj(Object thread, Object o);
    public static void newobj(Object o)
    {
 System.out.println("create new object");
 if ( engaged != 0 ) {
     _newobj(Thread.currentThread(), o);
 }
    }
   
    private static native void _newarr(Object thread, Object a);
    public static void newarr(Object a)
    {
 if ( engaged != 0 ) {
     _newarr(Thread.currentThread(), a);
 }
    }
   
}

关键在于如何向Object中注入那段代码,以及Trace的newobj方法的实现.在JDK1.5以前,在JVM已经启动的情况下是,把代码注入到java.lang.Object里就我目前所知是办不到的.另一种方法就是将jre的java.lang.Object用事先以经注入代码的一个"私有"的java.lang.Object替换调.初一看这种方法也不错,但这只是眼前的要求满足了.比如现在要求只检查java.lang.String耗费了多少内存,那我们又要替换它了.因此,一个比较好的方法是在类加载的时候来更改Class code,第一个想到的是定义自己的ClassLoader,问题也来了.在SUN的JDK里,bootclassloader会先加载jdk的核心类,根本就没有给我们的ClassLoader机会.在JDK1.5中,机会来了,就是通过JVMTI.


有关JVMTI的介绍,有很多的资料,大体的意思就是JVM提供了一些接口,你可以向
JVM注册你感兴趣的事件,比如我们这里感兴趣的是JVM加载类的时候,我们希望有
途径把我们自己的代码有选择的注入到类里.在JVMTI中,这个事件就是JVMTI_EVENT_CLASS_FILE_LOAD_HOOK .当JVM加载类的时候,会调用我们注册的回
调函数,而我们就可以在这个函数里做自己想做的事。


要实现这个目的,就要实现一个动态库,也就是JVMTI中的agent,在这个动态库中,
有两个函数是必须的:Agent_OnLoad和Agent_OnUnload,他们是JVM与代理交互的接口.当一个代理被JVM加载后,首先调用Agent_OnLoad这个方法;同样的,代理被JVM卸载后马上会调用Agent_OnUnload.我们就在Agent_OnLoad这个方法里注册感兴趣的事件,之后JVM就会在"有事"的时候通知我们.要使用agent,就要在启动JVM的时候告诉他要用到那个agent:
java -agentlib:<agentLibName>
这个agentLibName就是有我实现的动态库的名字,因系统而定,在Windows是agentLibName.dll,而Unix/Linux是agentLibName.so.

下面的代码简单的描述了这个结构:

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void
*reserved)
{
    //回调的接口
    jvmtiEventCallbacks    callbacks;   
    ....
    //告诉JVM我们对所有的类加载都感兴趣
    capabilities.can_generate_all_class_hook_events = 1;
    ....
    //注册处理类加载的函数,每当有类加载的时候,cbClassFileLoadHook所指
    // 向的函数就会被调用
    callbacks.ClassFileLoadHook = &cbClassFileLoadHook;
    ...
    //如果没有错误,返回JNI_OK,JVM会认为agent加载成功,否则会认为agent无
    效
    return JNI_OK;
}

JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm)
{
 //可以在这里做一些清除的工作
}

到这里基本的方向已经清楚了,可以分步实现了.我做的例子是参照jdk1.5的demo
做的,可以在这里找到%JAVA_HOME%\demo\jvmti\heapTracker
%JAVA_HOME%\demo\jvmti\java_crw_demo,基本上是对这两个例子的修修补补,先
说一下环境:

OS:WINDOWS 2000 SERVER
JDK:JDK1.5
C COMPILER:VC++ 6.0

由于C编程的经验少的可怜,在实现的过程遇到不少的问题,整个例子断断续续用
了三周的时间才大体上可用了:(

下面把我实现的过程大体讲一下,和大家分享,有错误的地方请指正或发邮件给我
agile.javaATgmail.com ^_^


1.首先由javah生成HeapTracker的头文件:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HeapTracker */

#ifndef _Included_HeapTracker
#define _Included_HeapTracker
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HeapTracker
 * Method:    _newobj
 * Signature: (Ljava/lang/Object;Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_HeapTracker__1newobj
  (JNIEnv *, jclass, jobject, jobject);

/*
 * Class:     HeapTracker
 * Method:    _newarr
 * Signature: (Ljava/lang/Object;Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_HeapTracker__1newarr
  (JNIEnv *, jclass, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif

2.实现这两个方法,这里我把他们放到我的agent的实现中,一个dll两种用途:)

/* Java Native Method for Object.<init> */
JNIEXPORT void JNICALL Java_HeapTracker__1newobj(JNIEnv *env, jclass klass, jthread thread, jobject o)
{
 jlong object_size = 0;
 jlong * size = &object_size; 
  printf("CREATE object,it's size is size %d\n",object_size);
 
}

/* Java Native Method for newarray */
JNIEXPORT void JNICALL Java_HeapTracker__1newarr(JNIEnv *env, jclass klass, jthread thread, jobject a)
{
 //未实现
}
 

3.实现agent,这个agent的实现基本上就是heapTracker的拷贝,但在实现的过程
  出了意想不到的情况。原来的heapTracker的实现,在cbClassFileLoadHook
  这个方法里,他调用了java_crw_demo这个dll中的方法,首先无法连接,没有
  lib;如果使用动态加载dll,那么在运行时会出错,说找不到java_crw_demo这个
  方法.为了能连接通过,把java_crw_demo重新编译,而java_crw_demo也没法编
  译,几个头文件找不到,jvm.h,opcodes.h和opcodes.length,其实这几个文件可
  以在jdk.1.5的source中找到,把java_crw_demo.c用到的一些定义拷贝到
  java_crw_demo.h就可以了.忘了说了,java_crw_demo在这里的工作就是完成代
  码的注入的.这样应该可以编译通过了.试着运行一下:
  java -Xbootclasspath/a:heapTracker.jar -agentlib:lm Test
  heapTraker.jar中是HeapTracker.class,-agentlib:lm中的
  heapTrakcer是我生成的dll名--lm.dll.
  结果呢?错误!为什么?因为VC6生成的 dll有个问题,就是在a.dll中分配的内存
  如果在b.dll中释放,会有运行时的异常,大体的意思是无效的指针.这是我在水
  木清华上无意中看到的一个文章.而这里的问题是heapTracker中用了由
  java_crw_demo修改过的Class Code的一个内存指针,其意思是在heapTracker
  用完之后由heapTracker释放这块内存,但vc6就是不行,不知道大家有没有遇到
  这样的情况,还是我做的不对?当时都已经想要放弃了:(.

  后来通过一个回调函数解决了,又得改java_crw_demo中的方法了:(.

  先改头文件java_crw_demo.h:
  //定义一个指针函数,用来回调用的
  typedef void (* crwhandler)(unsigned char * image,int len);


  typedef void (JNICALL *JavaCrwDemo)(
  .
  .     
  .
  //在最后加上回调
  crwhandler handler
 );

JNIEXPORT void JNICALL java_crw_demo(
  .
  .
  .
  //同样在最后加上回调
 crwhandler handler   
 );

定义一个crwhandler的实现方法:
static void handler(unsigned char * new_file_image,int newLength)
{
 if ( newLength > 0 ) { 
  enterCriticalSection(gdata->jvmti);
  {
   unsigned char *jvmti_space; 
   jvmti_space = (unsigned char *)allocate(gdata->jvmti, (jint)newLength);
   (void)memcpy((void*)jvmti_space, (void*)(new_file_image), (int)newLength);
   gdata->new_class_data_len = (jint)newLength;
   gdata->new_class_data = jvmti_space;  
  }
  exitCriticalSection(gdata->jvmti);
     } 
}

原来的hepaTracker.c中的cbClassFileLoadHook也要修改,在调用java_crw_demo
时,要在最后加上&handler,同时把它释放内存的代码去掉.

接下来修改java_crw_demo.c中的java_crw_demo方法:
 .
 .
 .
 /* Dispose or shrink the space to be returned. */
    if ( new_length == 0 ) {
 (void)free(new_image);
 new_image = NULL;
    } else {
        new_image = (void*)realloc((void*)new_image, (int)new_length);
 /* Return the new class image */
  *pnew_file_image = (unsigned char *)new_image;
  *pnew_file_len = (long)new_length;
  //own_handler是由调用者传如的回调函数,这里是由heapTracker.c中
 的cbClassFileLoadHook传入的
 if(own_handler != NULL)
 {   
  own_handler(*pnew_file_image,*pnew_file_len);
 }  
 //如果在代码外部free new_image,那么在vc6的环境一下,是不可操作的
 free(new_image);
 /* Cleanup before we leave. */
    }  
 .
 .
 .

到现在再运行,应该没有指针的异常了.可是有新的异常了
java.lang.StackOverflow:(

找了好久的原因,才发现向java.lang.Object中注入的代码有问题,现在注入的代
码类似于:
public Object(){
       HeapTracker.newobj(this);
}
这样子的话,在JVM启动的时候,要建立一些对象,会导致一个递归的调用,最后JVM
崩溃:(

因此,这里还需要对java_crw_demo做一些改动,使它在注入代码时要注入类似下
下面的代码:
  if(HeapTracker.engaged>0){
 HeapTracker.newobj(this);
   }
这涉及到class byte code 的知识,修改后的java_crw_demo.c中的
injection_template方法类似下面的代码:
 .
 .
 .
 if ( push_mnum ) {
        nbytes += push_short_constant_bytecodes(bytecodes+nbytes,
                    mi->number);
        }
    

/*
下面的代码是用来实现:
  if(HeapTracker.engaged>0){
     HeapTracker.newobj(this);
   }
*/
 bytecodes[nbytes++] = (ByteCode)opc_getstatic;
 bytecodes[nbytes++] = (ByteCode)(ci->trace_field_index >> 8);
 bytecodes[nbytes++] = (ByteCode)ci->trace_field_index;
 bytecodes[nbytes++] = (ByteCode)opc_ifne;
 bytecodes[nbytes++] = (ByteCode)0;
 bytecodes[nbytes++] = (ByteCode)3;
 bytecodes[nbytes++] = (ByteCode)opc_aload_0;
 .
 .
 .


至此,基本就可以工作了:)

最近工作忙,没时间研究jvmit了,只好先放一下,欢迎大家和我一起研究、学习。^_)

昨天晚上写完这些已经2点多了,还好今天没有迟到,不过忘记带门卡了:(

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多