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

2018年2月26日月曜日

C Java JVMTI

エージェント側のスレッド起動

JVMTIを使ってJVMエージェントを作ってみる その1
JVMTIを使ってJVMエージェントを作ってみる その2
JVMTIを使ってJVMエージェントを作ってみる その3
とは少し趣向を変えて、エージェント側のスレッドを起動してみよう。

スレッドの起動自体はRunAgentThreadを使って起動するんだけど、これはJavaのスレッドとして起動するため、Agent_OnLoad時ではなく、JVMが起動してから呼び出す必要がある。
このため、VMInitのイベントハンドラを登録して、そこからRunAgentThreadを呼び出す形になる。

スレッド起動のソースコード

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

static jvmtiEnv *jvmti = 0;
static volatile int stop = 0;

static void JNICALL jvmInit(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
                            jthread thread);
static void JNICALL jvmDeath(jvmtiEnv *jvmti_env, JNIEnv *jni_env);

static void JNICALL jvmStartFunction(jvmtiEnv *jvmti_env,
    JNIEnv *jni_env, void *arg) {
    while (!stop) {
        printf("thread is running.\n");
        sleep(1);
    }
} 

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

    jvmtiEventCallbacks callbackTable = { 0 };
    callbackTable.VMInit = jvmInit;
    callbackTable.VMDeath = jvmDeath;

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

    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_VM_INIT, NULL);
    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_VM_DEATH, 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 jvmInit(jvmtiEnv *jvmti_env, JNIEnv *jni_env,
    jthread thread) {
    jclass cls = (*jni_env)->FindClass(jni_env, "Ljava/lang/Thread;");
    jmethodID init = (*jni_env)->GetMethodID(jni_env, cls, "<init>", "()V");
    jthread thrd = (*jni_env)->NewObject(jni_env, cls, init);

    (*jvmti)->RunAgentThread(jvmti, thrd, jvmStartFunction, NULL,
        JVMTI_THREAD_MIN_PRIORITY);

    (*jni_env)->DeleteLocalRef(jni_env, thrd);
    (*jni_env)->DeleteLocalRef(jni_env, cls);

    printf("VM was init.\n");
} 

static void JNICALL jvmDeath(jvmtiEnv *jvmti_env, JNIEnv *jni_env) {
    stop = 1;
    printf("VM was dead.\n");
} 

Java側のソースコード

JVMTIのスレッドの確認をするためにJava側でも5秒スリープしておく。

local/Main.java
package local;

class Main {
    public static void main(String ... args) throws InterruptedException {
        System.out.println("Hello, JVMTI in Java.");
        Thread.sleep(5000);
        System.out.println("See you, JVMTI in Java.");
    }
}

コンパイル&実行

これをコンパイルして、実行すると以下になる。

$ 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...
VM was init.
thread is running.
Hello, JVMTI in Java.
thread is running.
thread is running.
thread is running.
thread is running.
thread is running.
See you, JVMTI in Java.
VM was dead.
agent is unloading...


エージェント側のスレッドを起動しておけば、イベントが発生しなくても、定期的なスレッドやヒープの情報取得に使えるね!