Résumé : Je constate des fuites de threads Java lorsque je rappelle en Java à partir d'un code natif sur un thread créé de manière native.
(Mise à jour du 11 février 2014 : Nous avons soulevé cette question en tant que demande d'assistance auprès d'Oracle. Elle a maintenant été confirmé par Oracle sur la mise à jour 45 de Java 7. Il n'affecte que les plateformes Linux 64 bits (et éventuellement Mac) : Linux 32 bits n'est pas affecté).
(Mise à jour du 29 avril 2014 : Oracle a un correctif pour ce problème, et il sera publié dans la mise à jour 80 de Java 7).
J'ai une application composée d'une couche Java et d'une bibliothèque native. La couche Java fait appel à la bibliothèque native via JNI : cela entraîne l'exécution d'un nouveau thread natif, qui rappelle Java. Comme le nouveau thread natif n'est pas attaché à la JVM, il doit être attaché avant de faire le callback, puis détaché après. La façon habituelle de procéder semble être de mettre entre parenthèses le code qui fait appel à Java avec des appels AttachCurrentThread / DetachCurrentThread. Cela fonctionne bien, mais pour notre application (qui fait très fréquemment appel à Java), les frais généraux liés à l'attachement et au détachement à chaque fois sont importants.
Il existe une optimisation décrite à plusieurs endroits (par ex. ici et ici ) qui recommande d'utiliser des mécanismes basés sur le stockage local des threads pour éliminer ce problème : essentiellement, chaque fois que le callback natif est déclenché, le thread est testé pour voir s'il est déjà attaché à la JVM : si ce n'est pas le cas, il est attaché à la JVM et le mécanisme de stockage local des threads est utilisé pour détacher automatiquement le thread lorsqu'il sort. J'ai implémenté cette méthode, mais bien que l'attachement et le détachement semblent se produire correctement, cela provoque une fuite de threads du côté Java. Je pense que je fais tout correctement et j'ai du mal à trouver ce qui ne va pas. Cela fait un moment que je me casse la tête sur ce problème et je vous serais très reconnaissant de me donner votre avis.
J'ai recréé le problème sous forme réduite. Voici le code de la couche native. Nous avons ici un wrapper qui encapsule le processus de retour d'un pointeur JNIEnv pour le thread actuel, en utilisant le mécanisme POSIX de stockage local des threads pour détacher automatiquement le thread s'il n'était pas déjà attaché. Il existe une classe callback qui agit comme un proxy pour la méthode Java callback. (J'ai utilisé le callback vers une méthode Java statique afin d'éliminer la complication supplémentaire de la création et de la suppression des références globales à l'objet Java, qui ne sont pas pertinentes pour ce problème). Enfin, il existe une méthode JNI qui, lorsqu'elle est appelée, construit un callback, crée un nouveau thread natif et attend qu'il soit terminé. Ce nouveau thread appelle la callback une fois, puis se termine.
#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;
}
}
}
Le code Java est très simple : il appelle de manière répétée la méthode native dans une boucle :
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();
}
Vous pouvez voir que, bien que la couche native signale que le thread natif est attaché et détaché avec succès, chaque fois que la fonction de rappel est déclenchée, un nouveau thread Java est créé :
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
Juste pour ajouter : la plateforme de développement que j'utilise est CentOS 6.3 (64 bits). La version de Java est la version 1.7.0_45 de la distribution Oracle, bien que le problème se manifeste également avec la distribution OpenJDK, versions 1.7 et 1.6.