[Java] JVMTIを使ってJVMエージェントを作ってみる その1

2018年2月5日月曜日

C Java JVMTI

JVMTI

Javaのコードを書き換えることなく、実行中にスレッドやヒープの情報を取得するツールはいろいろと存在する。
これらのツールのようにJavaの実行環境情報を実行中に取得するにはどうしたらいいんだろう。

JNIでクラス検索するときのパッケージ名ではJNIを使って、CからJVMを起動したけど、今度はJVMTIを使って、JVMから呼び出されるCのコードを作ってみよう。

JVITI(JVM Tool Interface)
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html

※今回試した実行環境はJDK8, openSUSE 42.2

Java側のサンプル

まずは監視対象となるJavaのサンプルを用意。ただのHello world。

local/Main.java
package local;

class Main {
    public static void main(String ... args) {
        System.out.println("Hello, JVMTI in Java.");
    }
}

エージェント側

JVMTIではJVMから呼び出される共有ライブラリとして作成する必要がある。
この共有ライブラリをJava起動時にエージェントとして指定すれば、JVMから呼び出してくれる。

jvmtitest.c
#include <stdio.h>
#include <string.h>
#include <jvmti.h>

static jvmtiEnv *jvmti = 0;

static void JNICALL ClassPrepare(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                                 jthread thread, jclass klass);
static jstring getClassName(JNIEnv *env, jclass klass);

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    (*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION);

    jvmtiEventCallbacks callbackTable = { 0 };
    callbackTable.ClassPrepare = ClassPrepare;

    (*jvmti)->SetEventCallbacks(jvmti, &callbackTable, sizeof(callbackTable));

    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_CLASS_PREPARE, NULL);
    
    printf("agent is loading...\n");
    return 0;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    (*jvmti)->DisposeEnvironment(jvmti);
    printf("agent is unloading...\n");
}

static void JNICALL ClassPrepare(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                                 jthread thread, jclass klass) {
    jstring strObj = getClassName(jni_env, klass);
    const char *str = (*jni_env)->GetStringUTFChars(jni_env, strObj, NULL);

    if (strcmp(str, "local.Main") == 0) {
        printf("local.Main prepared.\n");
    }

    (*jni_env)->ReleaseStringUTFChars(jni_env, strObj, str);
}

static jstring getClassName(JNIEnv *env, jclass klass) {
    jclass cls = (*env)->GetObjectClass(env, klass);
    jmethodID mid =
         (*env)->GetMethodID(env, cls, "getName", "()Ljava/lang/String;"); 
    jstring strObj = (jstring)(*env)->CallObjectMethod(env, klass, mid);

    return strObj;
}

基本的なスタイルとしてはAgent_OnLoad,Agent_OnUnload関数を作成しておくと、JVMTIエージェントのロード時、アンロード時に呼び出してくれるため、このタイミングでイベントハンドラを登録することになる。
今回の場合はクラスの準備ができたときに呼び出されるClassPrepareを登録している。
これで、各クラスの準備ができる度にClassPrepareイベントのハンドラが呼び出されるため、クラス名を取得して、クラス名が"local.Main"の場合のみ、printfで出力するようにした。

また、JNIのJNIEnvと同様にJVMTIの関数はjvmtiEnvを通して参照するため、Agent_OnLoad時にJVMTIEnvを初期化しておく必要がある。

コンパイル&実行

これをコンパイルして、実行すると、以下のように出力される。
JVMTIのヘッダファイルをインクルードする必要があるため、JDKのインストール先のincludeと環境依存のinclude/xxxをインクルードパスに追加しておこう。

$ javac local/Main.java
$ gcc -Wall -std=c99 -shared -fPIC -D_REENTRANT -o libjvmtitest.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux jvmtitest.c
$ java -agentpath:./libjvmtitest.so local.Main
agent is loading...
local.Main prepared.
Hello, JVMTI in Java.
agent is unloading...


  1. エージェントのロード
  2. local.Mainクラス準備イベント発生
  3. mainメソッド実行
  4. エージェントのアンロード
の順で、処理が発生していることがわかる。