Подтвердить что ты не робот

Почему я не должен повторно использовать jclass и/или jmethodID в JNI?

Это вопрос, связанный с предыдущим сообщением, но этот пост был решен, и теперь я хотел изменить направление вопроса.

При работе с JNI необходимо задать объект JNIEnv для jclass и jmethodID для каждого класса и метода, которые будут использоваться в коде C/С++. Чтобы быть ясным, я хочу называть Java-конструкторы или методы из C/С++.

Поскольку связь с Java на C/С++ (и наоборот) является дорогостоящей, изначально я думал, что одним из способов свести к минимуму это было повторное использование jclass и jmethodID. Поэтому я сохранил эти экземпляры в глобальных переменных следующим образом:

jclass    someClass  = NULL;
jmethodID someMethod = NULL;

JNIEXPORT jobject JNICALL Java_example_method1(JNIEnv *env, jobject jobj) {
    // initialize someClass and someMethod if they are NULL
    // use someClass and someMethod to call java (for example, thru NewObject)
}

JNIEXPORT jobject JNICALL Java_example_method2(JNIEnv *env, jobject jobj) {
    // initialize someClass and someMethod if they are NULL
    // use someClass and someMethod to call java again
}

Более конкретный (и полезный) пример, который я использую для исключения исключений из любого места в своих функциях JNI:

jclass    jniExceptionClass           = NULL;

void throwJavaException(JNIEnv *env, const char* msg) {
    if (!jniExceptionClass) {
        jniExceptionClass = env->FindClass("example/JNIRuntimeException");
    }
    if (jniExceptionClass)
        env->ThrowNew(jniExceptionClass, msg);
    }
}

Проблема в том, что я продолжал использовать этот шаблон и получил ошибку сегментации, которая была решена только путем повторного использования этих переменных (это было решением предыдущего сообщения).

Вопросы:

  • Почему не разрешено повторно использовать jclass и jmethodID через различные функции JNI? Я думал, что эти ценности всегда совпадают.
  • Просто для любопытства: каково влияние/накладные расходы на инициализацию всех необходимых jclass и jmethodID для каждой функции JNI?
4b9b3361

Ответ 1

Правила здесь ясны. Идентификатор метода и значения идентификатора поля навсегда. Вы можете висеть на них. Поиск займет некоторое время.

jclass, с другой стороны, обычно является локальной ссылкой. Местная ссылка выживает, самое большее, длительность одного вызова функции JNI.

Если вам нужно оптимизировать, вы должны попросить JVM сделать глобальную ссылку для вас. Это не редкость для получения и хранения ссылок на общие классы, такие как java.lang.String.

Сохранение такой ссылки на класс, конечно, не позволит этому (классу) собираться в мусор.

jclass local = env->FindClass(CLS_JAVA_LANG_STRING);
_CHECK_JAVA_EXCEPTION(env);
java_lang_string_class = (jclass)env->NewGlobalRef(local);
_CHECK_JAVA_EXCEPTION(env);
env->DeleteLocalRef(local);
_CHECK_JAVA_EXCEPTION(env);

Вывод макросов проверки:

static inline void
check_java_exception(JNIEnv *env, int line)
{
    UNUSED(line);
    if(env->ExceptionOccurred()) {
#ifdef DEBUG
        fprintf(stderr, "Java exception at rlpjni.cpp line %d\n", line);
        env->ExceptionDescribe();
    abort();
#endif
        throw bt_rlpjni_java_is_upset();
    }
}

Ответ 2

Внутри JNI_OnLoad вам нужно использовать NewGlobalRef для значений jclass, возвращаемых FindClass, перед их кешированием.

Затем внутри JNI_OnUnload вы вызываете DeleteGlobalRef на них.

Ответ 3

Как уже писали другие

  1. Вы можете без проблем хранить jmethodID в статической переменной C++
  2. Вы можете сохранить локальные jobject или jclass в статической переменной C++ после преобразования их в глобальные объекты, вызвав env->NewGloablRef()

Я просто хочу добавить дополнительную информацию здесь: Основная причина хранения jclass в статической переменной C++ заключается в том, что вы думаете, что это проблема с производительностью - каждый раз вызывать env->FindClass().

Но я измерил скорость FindClass() с помощью счетчика производительности с помощью API QueryPerformanceCounter() в Windows. И результат был удивительным:

На моем компьютере с процессором 3,6 ГГц выполнение

jcass p_Container = env->FindClass("java/awt/Container");

занимает от 0,01 мс до 0,02 мс. Это невероятно быстро. Я посмотрел в исходный код Java, и они используют словарь, где хранятся классы. Кажется, это реализовано очень эффективно.

Я проверил еще несколько классов, и вот результат:

Elapsed 0.002061 ms for java/net/URL
Elapsed 0.044390 ms for java/lang/Boolean
Elapsed 0.019235 ms for java/lang/Character
Elapsed 0.018372 ms for java/lang/Number
Elapsed 0.017931 ms for java/lang/Byte
Elapsed 0.017589 ms for java/lang/Short
Elapsed 0.017371 ms for java/lang/Integer
Elapsed 0.015637 ms for java/lang/Double
Elapsed 0.018173 ms for java/lang/String
Elapsed 0.015895 ms for java/math/BigDecimal
Elapsed 0.016204 ms for java/awt/Rectangle
Elapsed 0.016272 ms for java/awt/Point
Elapsed 0.001817 ms for java/lang/Object
Elapsed 0.016057 ms for java/lang/Class
Elapsed 0.016829 ms for java/net/URLClassLoader
Elapsed 0.017807 ms for java/lang/reflect/Field
Elapsed 0.016658 ms for java/util/Locale
Elapsed 0.015720 ms for java/lang/System
Elapsed 0.014669 ms for javax/swing/JTable
Elapsed 0.017276 ms for javax/swing/JComboBox
Elapsed 0.014777 ms for javax/swing/JList
Elapsed 0.015597 ms for java/awt/Component
Elapsed 0.015223 ms for javax/swing/JComponent
Elapsed 0.017385 ms for java/lang/Throwable
Elapsed 0.015089 ms for java/lang/StackTraceElement

Приведенные выше значения взяты из потока диспетчера событий Java. Если я выполню тот же код в собственном потоке Windows, который был создан мной CreateThread(), он будет работать даже в в 10 раз быстрее. Почему?

Так что если вы не вызываете FindClass() очень часто, то нет абсолютно никаких проблем с вызовом его по требованию, когда ваша функция JNI вызывается вместо создания глобальной ссылки и сохранения ее в статической переменной.


Еще одна важная тема - безопасность потоков. В Java каждый поток имеет свою собственную независимую структуру JNIEnv.

  1. Глобальные jobject или jclass действительны в любом потоке Java.
  2. Локальные объекты действительны только в одном вызове функции в JNIEnv вызывающего потока и собирают мусор, когда код JNI возвращается в Java.

Теперь это зависит от потоков, которые вы используете: если вы регистрируете свою функцию C++ с помощью env->RegisterNatives() и Java-код вызывает ваши функции JNI, то вы должны сохранить все объекты, которые вы хотите использовать позже, в качестве глобальных объектов, в противном случае они будут собирать мусор.

Но если вы создадите свой собственный поток с помощью API CraeteThread() (в Windows) и получите структуру JNIEnv, вызвав AttachCurrentThreadAsDaemon(), тогда будут применяться совершенно другие правила: поскольку это ваш собственный поток, который никогда не возвращает управление Java, сборщик мусора никогда не будет очищать объекты, созданные вами в вашем потоке, и вы даже можете без проблем хранить локальные объекты в статических переменных C++ (но к ним нельзя получить доступ из других потоков). В этом случае крайне важно, чтобы вы очистили ВСЕ ваши локальные экземпляры вручную с помощью env->DeleteLocalRef(), иначе у вас будет утечка памяти. (Однако сборщик мусора, вероятно, освобождает все локальные объекты при вызове DetachCurrentThread(), который я никогда не проверял.)

Я настоятельно рекомендую загружать ВСЕ локальные объекты в класс-оболочку, который вызывает деструктор DeleteLocalRef(). Это пуленепробиваемый способ избежать утечек памяти.


Разработка кода JNI может быть очень громоздкой, и вы можете получить сбои, которые вы не понимаете. Чтобы найти причину сбоя, откройте окно DOS и запустите приложение Java с помощью следующей команды:

java -Xcheck:jni -jar MyApplication.jar

тогда вы увидите, какие проблемы произошли в коде JNI. Например:

FATAL ERROR in native method: Bad global or local ref passed to JNI

и вы найдете трассировку стека в файле журнала, который Java создает в той же папке, где у вас есть файл JAR:

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e8655d5, pid=4692, tid=4428
#
etc...

Ответ 4

Как я помню, jclass будет локальным для вызывающего метода, поэтому его нельзя кэшировать, однако идентификатор метода может быть. Подробнее см. http://java.sun.com/javase/6/docs/technotes/guides/jni/spec/design.html.

Извините, я не знаю об аспекте производительности, в любое время, когда я использовал JNI, это было незначительно по сравнению с задачей в руке.

Ответ 5

Вы можете кэшировать и использовать ваши идентификаторы методов/функций/классов и безопасно использовать их, если вы правильно его кодируете. Я ответил на аналогичный вопрос здесь на SO и опубликовал явный код того, как следовать рекомендациям по производительности кэширования IBM.