web-dev-qa-db-ja.com

Java JNIを介してネイティブスレッドからコールバックするとスレッドがリークする

概要:ネイティブに作成されたスレッドのネイティブコードからJavaにコールバックすると、Javaスレッドリークが発生します。

(2014年2月11日更新:これをOracleのサポートリクエストとして提起しました。現在、OracleはJava 7 update45で 確認済み です。64にのみ影響します。ビットLinux(および場合によってはMac)プラットフォーム:32ビットLinuxは影響を受けません)。

(2014年4月29日更新:Oracleにはこの問題の修正があり、Java 7 update 80)でリリースされます。

Javaレイヤーとネイティブライブラリで構成されるアプリケーションがあります。JavaレイヤーはJNIを介してネイティブライブラリを呼び出します。これにより、新しいネイティブスレッドが発生します。実行を開始するには、Javaを呼び出します。新しいネイティブスレッドはJVMにアタッチされていないため、コールバックを実行する前にアタッチし、後でデタッチする必要があります。これを行う通常の方法は、コードを括弧で囲むことです。 Java with AttachCurrentThread/DetachCurrentThread呼び出しでコールバックします。これは正常に機能しますが、アプリケーション(Java非常に頻繁にコールバック))の場合、アタッチのオーバーヘッド毎回切り離すことが重要です。

この問題を排除するためにスレッドローカルストレージに基づくメカニズムを使用することを推奨するいくつかの場所( ここここ など)で説明されている最適化があります:本質的にネイティブコールバックがトリガーされるたびに、スレッドは、JVMにすでに接続されているかどうかを確認するためにテストされます。接続されていない場合は、JVMに接続され、スレッドローカルストレージメカニズムを使用して、スレッドが終了したときにスレッドが自動的に切り離されます。私はこれを実装しましたが、アタッチとデタッチが正しく行われているように見えますが、これによりJava側でスレッドのリークが発生します。私はすべてを正しく行っており、何を確認するのに苦労していると思います間違っているかもしれません。私はしばらくの間これに頭を悩ませてきました、そして私はどんな洞察にも非常に感謝するでしょう。

問題を切り詰めた形で再現しました。以下は、ネイティブレイヤーのコードです。ここにあるのは、現在のスレッドのJNIEnvポインターを返すプロセスをカプセル化するラッパーであり、POSIXスレッドローカルストレージメカニズムを使用して、スレッドがまだ接続されていない場合は自動的に切り離します。 Javaコールバックメソッドのプロキシとして機能するコールバッククラスがあります。(余分なものを排除するために、静的Javaメソッドへのコールバックを使用しました) Javaオブジェクト、この問題には関係ありません)へのグローバルオブジェクト参照の作成と削除の複雑さ。最後に、呼び出されたときにコールバックを構築し、新しいネイティブスレッドを作成するJNIメソッドがあります。この新しく作成されたスレッドは、コールバックを1回呼び出してから、終了します。

#include <jni.h>
#include <iostream>
#include <pthread.h>


using namespace std;


/// Class to automatically handle getting thread-specific JNIEnv instance,
/// and detaching it when no longer required
class JEnvWrapper
{

public:

    static JEnvWrapper &getInstance()
    {
        static JEnvWrapper wrapper;
        return wrapper;
    }

    JNIEnv* getEnv(JavaVM *jvm)
    {
        JNIEnv *env = 0;
        jint result = jvm->GetEnv((void **) &env, JNI_VERSION_1_6);
        if (result != JNI_OK)
        {
            result = jvm->AttachCurrentThread((void **) &env, NULL);
            if (result != JNI_OK)
            {
                cout << "Failed to attach current thread " << pthread_self() << endl;
            }
            else
            {
                cout << "Successfully attached native thread " << pthread_self() << endl;
            }

            // ...and register for detach when thread exits
            int result = pthread_setspecific(key, (void *) env);
            if (result != 0)
            {
                cout << "Problem registering for detach" << endl;
            }
            else
            {
                cout << "Successfully registered for detach" << endl;
            }
        }

        return env;
    }

private:

    JEnvWrapper()
    {
        // Initialize the key
        pthread_once(&key_once, make_key);
    }

    static void make_key()
    {
        pthread_key_create(&key, detachThread);
    }


    static void detachThread(void *p)
    {
        if (p != 0)
        {
            JavaVM *jvm = 0;
            JNIEnv *env = (JNIEnv *) p;
            env->GetJavaVM(&jvm);
            jint result = jvm->DetachCurrentThread();
            if (result != JNI_OK)
            {
                cout << "Failed to detach current thread " << pthread_self() << endl;
            }
            else
            {
                cout << "Successfully detached native thread " << pthread_self() << endl;
            }

        }
    }


    static pthread_key_t key;
    static pthread_once_t key_once;
};

pthread_key_t JEnvWrapper::key;
pthread_once_t JEnvWrapper::key_once = PTHREAD_ONCE_INIT;



class Callback
{

public:

    Callback(JNIEnv *env, jobject callback_object)
    {
        cout << "Constructing callback" << endl;
        const char *method_name = "javaCallback";
        const char *method_sig = "(J)V";

        env->GetJavaVM(&m_jvm);

        m_callback_class = env->GetObjectClass(callback_object);
        m_methodID = env->GetStaticMethodID(m_callback_class, method_name, method_sig);
        if (m_methodID == 0)
        {
            cout << "Couldn't get method id" << endl;
        }
    }

    ~Callback()
    {
        cout << "Deleting callback" << endl;
    }

    void callback()
    {
        JNIEnv *env = JEnvWrapper::getInstance().getEnv(m_jvm);
        env->CallStaticVoidMethod(m_callback_class, m_methodID, (jlong) pthread_self());
    }

private:

    jclass m_callback_class;
    jmethodID m_methodID;
    JavaVM *m_jvm;
};



void *do_callback(void *p)
{
    Callback *callback = (Callback *) p;
    callback->callback();
    pthread_exit(NULL);
}




extern "C"
{

JNIEXPORT void JNICALL Java_com_test_callback_CallbackTest_CallbackMultiThread(JNIEnv *env, jobject obj)
{
    Callback callback(env, obj);
    pthread_t thread;
    pthread_attr_t attr;
    void *status;
    int rc;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    rc = pthread_create(&thread, &attr, do_callback, (void *) &callback);
    pthread_attr_destroy(&attr);
    if (rc)
    {
        cout << "Error creating thread: " << rc << endl;
    }
    else
    {
        rc = pthread_join(thread, &status);
        if (rc)
        {
            cout << "Error returning from join " << rc << endl;
        }
    }
}

Javaコードは非常に単純です。ループ内でネイティブメソッドを繰り返し呼び出すだけです。

package com.test.callback;

public class CallbackTest
{

    static
    {
        System.loadLibrary("Native");
    }

    public void runTest_MultiThreaded(int trials)
    {
        for (int trial = 0; trial < trials; trial++)
        {
            // Call back from this thread
            CallbackMultiThread();

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    static void javaCallback(long nativeThread)
    {
        System.out.println("Java callback: native thread: " + nativeThread + ", Java thread: " + Thread.currentThread().getName() + ", " + Thread.activeCount() + " active threads");
    }

    native void CallbackMultiThread();  
}

以下は、このテストの出力例です。ネイティブレイヤーは、ネイティブスレッドが正常にアタッチおよびデタッチされたことを報告していますが、コールバックがトリガーされるたびに、新しいJavaスレッドは作成した:

Constructing callback
Successfully attached native thread 140503373506304
Successfully registered for detach
Java callback: native thread: 140503373506304, Java thread: Thread-67, 69 active threads
Successfully detached native thread 140503373506304
Deleting callback
Constructing callback
Successfully attached native thread 140503373506304
Successfully registered for detach
Java callback: native thread: 140503373506304, Java thread: Thread-68, 70 active threads
Successfully detached native thread 140503373506304
Deleting callback
Constructing callback
Successfully attached native thread 140503373506304
Successfully registered for detach
Java callback: native thread: 140503373506304, Java thread: Thread-69, 71 active threads
Successfully detached native thread 140503373506304
Deleting callback
Constructing callback
Successfully attached native thread 140503373506304
Successfully registered for detach
Java callback: native thread: 140503373506304, Java thread: Thread-70, 72 active threads
Successfully detached native thread 140503373506304
Deleting callback
Constructing callback
Successfully attached native thread 140503373506304
Successfully registered for detach
Java callback: native thread: 140503373506304, Java thread: Thread-71, 73 active threads
Successfully detached native thread 140503373506304
Deleting callback

追加するだけです。私が使用している開発プラットフォームはCentOS6.3(64ビット)です。 JavaバージョンはOracleディストリビューションバージョン1.7.0_45ですが、OpenJDKディストリビューションのバージョン1.7および1.6でも問題が発生します。

55
Malcolm Wilkins

OracleはJVMでこの問題を修正しており、Java 7 update80でリリースされる予定です。

あなたがあなた自身の答えを受け入れるつもりがないなら、多分あなたはこれを受け入れるでしょう。少なくとも、ゼロアンサーの質問に対してそれほど多くのトラフィックを引き込むことはもうありません。

6
David Newcomb