[Java How To Programming] [Home on 246net] [Home on Alles net]
Powered by SmartDoc

JVMTIを使う

TAKAHASHI,Toru
torutk@alles.or.jp

目次

JVMTIを利用する

JVMTI:Java Virtual Machine Tool Interfaceは、デバッグ・プロファイリングに有用なJava仮想マシンのインタフェースです。JavaVM上の様々な動きを取り出すことができます。

HelloAgent

まず、極めて簡単なJVMTIエージェントを定義して組み込んでみましょう。

JVMTIエージェントはネイティブライブラリの形式で作成され、Javaを実行する際にVMオプションで指定することによって使用可能となります。

まず最初のJVMTIエージェントとして、ライブラリのロード時およびアンロード時にメッセージを表示する機能を実装します。

ライブラリがロードされるとまずAgent_OnLoad関数が呼び出されます。第2引数のoptionsには、JavaVM起動時にVMオプションで指定した文字列が設定されます。この関数の戻り値にJNI_OK以外を返却させると、JavaVMは強制終了します。

JavaVM終了時にはライブラリをアンロードするため、Agent_OnUnload関数が呼び出されます。

HelloAgent
/**
 * HelloAgent.c
 * 極簡単JVMTIエージェント
 */
#include <jvmti.h>
#include <stdio.h>

/**
 * JVMTIエージェントが含まれたライブラリがロードされた際に呼び出される。
 * options に'abort'が指定されると、JNI_ERRを返却する
 */
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* \
    reserved) {
  printf("Hello, JVMTI. option '%s' specified\n", options);
  if (options != NULL && strcmp("abort", options) == 0) {
    return JNI_ERR;
  } else {
    return JNI_OK;
  }
}

/**
 * JavaVMが終了する際に呼び出される。
 */
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM* vm) {
  printf("Good-bye, JVMTI.\n");
}

コンパイル in Windows

Windows用のエージェントライブラリをビルドします。環境には、VC++ Toolkit 2003を使用しています。jvmti.hは、JDK1.5.0をインストールしたディレクトリの中にあるので、インクルードパスを設定します。

JVMTIエージェントのビルド
D:\work\helloagent> cl /Id:\java\jdk1.5.0\include \
    /Id:\java\jdk1.5.0\include\win32 /c HelloAgent.c

HelloAgent.c

D:\work\helloagent> link /dll HelloAgent.obj

D:\work\helloagent>

実行 in Windows

JVMTIエージェントの実行
D:\work\helloagent> java -agentlib:HelloAgent -jar test.jar
Hello, JVMTI. option '(null)' specified
Good-bye, JVMTI.
D:\work\helloagent>

Agent_OnLoad関数の第2引数optionsに文字列を渡すには、次のように指定します。

JVMTIエージェントの実行(オプション指定有)
D:\work\helloagent> java -agentlib:HelloAgent=bastard -jar \
    test.jar
Hello, JVMTI. option '(bastard)' specified
Good-bye, JVMTI.
D:\work\helloagent>

TraceAgent

メソッド呼び出しをトレースするJVMTIエージェントを作成します。ただし、ここで実装する方法は負荷が高いので、プロファイリング目的でメソッドトレースを行うなら、バイトコード・インスツルメンテーションを用いるべきとの記載がSunのドキュメントにあります。

TraceAgent
/**
 * TraceAgent.c
 * メソッド実行トレース簡単JVMTIエージェント
 */
#include <jvmti.h>
#include <stdio.h>

static jvmtiEnv* jvmti = NULL;
static jvmtiCapabilities capa;

/**
 * JavaVM上でメソッドが実行されたら呼び出される。
 * 
 */
static void JNICALL
methodEntry(jvmtiEnv* jvmti, JNIEnv* env, jthread thread, jmethodID method) \
    {
  jvmtiError error;
  char* name;
  char* sig;
  char* gsig;

  error = (*jvmti)->GetMethodName(jvmti, method, &name, &sig, &gsig);
  if (error != JVMTI_ERROR_NONE) {
    printf("GetMethodName:%d\n", error);
    return;
  }
  printf("MethodEntry:%s%s\n", name, sig);
}

/**
 * JavaVMが初期化される際に呼び出されるコールバック関数。
 * Agent_OnLoad内でコールバックとして登録している。
 */
static void JNICALL vmInit(jvmtiEnv* jvmti, JNIEnv* env, jthread thread) {
  printf("JVMTI_EVENT_VM_INIT notified\n");
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, \
    JVMTI_EVENT_METHOD_ENTRY, NULL);
}

/**
 * JVMTIエージェントが含まれたライブラリがロードされた際に呼び出される。
 * options に'abort'が指定されると、JNI_ERRを返却する
 */
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* \
    reserved) {
  jvmtiError error;
  jint ret;
  jvmtiEventCallbacks callbacks;

  printf("Hello, JVMTI. option '%s' specified\n", options);

  ret = (*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0);
  if (ret != JNI_OK || jvmti == NULL) {
    printf("ERROR: Unable to access JVMTI Version 1 (0x%x),"
	   "is your J2SE a 1.5 or newer version?"
	   "JNIEnv's GetEnv() returned %d\n", JVMTI_VERSION_1, ret);
  }

  (void)memset(&capa, 0, sizeof(jvmtiCapabilities));
  capa.can_generate_method_entry_events = 1;
  error = (*jvmti)->AddCapabilities(jvmti, &capa);
  if (error != JVMTI_ERROR_NONE) {
    printf("AddCapabilities:%d\n", error);
    return JNI_ERR;
  }
  
  memset(&callbacks, 0, sizeof(callbacks));
  callbacks.VMInit = &vmInit;
  callbacks.MethodEntry = &methodEntry;
  (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));
(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, \
    JVMTI_EVENT_VM_INIT, NULL);
  return JNI_OK;
}

/**
 * JavaVMが終了する際に呼び出される。
 */
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM* vm) {
  printf("Good-bye, JVMTI.\n");
}

コールバックの登録と通知開始設定

JVMTIでは、JavaVM上の活動をイベントとしてコールバック関数によって受け取ることができます。TraceAgentでは、以下の2つのイベント通知を受けるようになっています。

  1. JavaVMの初期化が実施されたことを示すJVMTI_EVENT_VM_INITイベント
  2. JavaVM上でメソッドが実行されたことを示すJVMTI_EVENT_METHOD_ENTRYイベント

コールバック関数は、イベント種類に応じて引数・戻り値型が規定されています。その規定に合わせた関数(名前は任意に定義可能)を用意しています。

  1. JVMTI_EVENT_VM_INITのコールバックvmInit関数
  2. JVMTI_EVENT_METHOD_ENTRYのコールバックmethodEntry関数

定義したコールバック関数をJVMTIに登録するために、jvmtiEventCallbacks構造体の対応するフィールドに関数をセットし、続いて、JVMTI環境のSetEventCallbacks関数を呼んで登録しています。

ここで、JVMTI_EVENT_METHOD_ENTRYイベントを取るためには、権限としてgenerate_method_entry_eventsが必要になるので、事前にjvmtiCapabilities構造体の対応フィールドに1をセットし、JVMTI環境のAddCapabilities関数を呼んで設定しています。

次に、コールバック関数を登録しただけではイベント通知は行われないので、JVMTI環境のSetEventNotificationMode関数で通知を有効に設定します。ここでは、まずエージェントライブラリがロードされた際にJVMTI_EVENT_VM_INITの通知開始を指定し、JVMTI_EVENT_VM_INITイベント通知のコールバック関数(vmInit)の中で、JVMTI_EVENT_METHOD_ENTRYイベントの通知開始を指定しています。

メソッド処理開始コールバック

methodEntryコールバック関数では、メソッド名とシグニチャ名を引数を元に取り出してプリントしています。