静态 C++ 对象中的 JNI 环境指针并调用 java 函数连续两次使用字符串参数会使 JVM 崩溃
JNI Environment pointer in a static c++ object and calling a java function taking a string argument twice in a row crashes the JVM
因此,根据评论员的要求,我终于找到了一个重现我的错误的 MCVE。所以一般的设置是 Java 使用 JNI 调用一个 dll,dll 抓住 运行 JVM 并存储一个指向 JNIEnv 的指针,它用来调用 java class(从 C++ 调用的 java class 不一定是原始调用 java 对象,这就是输入 jobject 不用于回调的原因) .在我进一步解释之前,让我 post 所有代码:
JniTest.java
package jnitest;
public class JniTestJava {
public static void main(String[] args) {
try {
System.load("<path-to-dll>");
} catch (Throwable e) {
e.printStackTrace();
}
DllFunctions dllFunctions = new DllFunctions();
dllFunctions.setup();
dllFunctions.singleIntFunctionCall();
dllFunctions.doubleIntFunctionCall();
dllFunctions.singleStringFunctionCall();
dllFunctions.doubleStringFunctionCall();
}
public void javaStringFunction(String input){
System.out.println(input);
}
public void javaIntFunction(int input){
System.out.println(input);
}
}
DllFunctions.java
package jnitest;
public class DllFunctions{
public native void singleIntFunctionCall();
public native void doubleIntFunctionCall();
public native void singleStringFunctionCall();
public native void doubleStringFunctionCall();
public native void setup();
}
JniTestCpp.h
#include <jni.h>
#ifndef _Included_jnitest_JniTestJava
#define _Included_jnitest_JniTestJava
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject);
#ifdef __cplusplus
}
#endif
#endif
JniTestCpp.cpp
#include "JniTestCpp.h"
#include "JniTestClass.h"
JniTestClass jniTestClass;
extern "C"
{
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
jniTestClass.setup();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaIntFunction();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaStringFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaStringFunction();
jniTestClass.callJavaStringFunction();
}
}
JniTestClass.h
#include <jni.h>
class JniTestClass {
typedef jint(JNICALL * GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
public:
void setup();
void callJavaStringFunction();
void callJavaIntFunction();
void throwException(jthrowable ex);
private:
jobject myObject;
jclass myClass;
JNIEnv* env;
};
JniTestClass.cpp
#include "JniTestClass.h"
#include <Windows.h>
#include <fstream>
void JniTestClass::setup() {
jint jni_version = JNI_VERSION_1_4;
GetCreatedJavaVMs jni_GetCreatedJavaVMs;
jsize nVMs = 0;
jni_GetCreatedJavaVMs = (GetCreatedJavaVMs) GetProcAddress(GetModuleHandle(
TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs");
jni_GetCreatedJavaVMs(NULL, 0, &nVMs);
JavaVM** buffer = new JavaVM*[nVMs];
jni_GetCreatedJavaVMs(buffer, nVMs, &nVMs);
buffer[0]->GetEnv((void **) &env, jni_version);
delete buffer;
myClass = env->FindClass("jnitest/JniTestJava");
myObject = env->NewObject(myClass, env->GetMethodID(myClass, "<init>", "()V"));
}
void JniTestClass::callJavaStringFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->CallVoidMethod(myObject, myMethod, env->NewStringUTF("String!"));
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
}
void JniTestClass::callJavaIntFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaIntFunction", "(I)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->CallVoidMethod(myObject, myMethod, 1);
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
}
void JniTestClass::throwException(jthrowable ex) {
env->ExceptionClear();
jclass clazz = env->GetObjectClass(ex);
jmethodID getMessage = env->GetMethodID(clazz,
"toString",
"()Ljava/lang/String;");
jstring message = (jstring) env->CallObjectMethod(ex, getMessage);
const char *mstr = env->GetStringUTFChars(message, NULL);
printf("%s \n", mstr);
throw std::runtime_error(mstr);
}
这里的意图是 JniTestCpp 应该只有 JNI 导出函数并且没有声明 classes。 JniTestClass 背后的想法是它应该保存所有 JNI 指针和变量(对象、class 和环境指针)并提供 JniTestCpp 可以使用的方法。
现在,此代码的显示方式在 JniTest.java 中的 dllFunctions.doubleStringFunctionCall();
调用时崩溃,输出如下:
1
1
1
String!
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e306515, pid=1268, tid=8028
#
# JRE version: Java(TM) SE Runtime Environment (7.0_80-b15) (build 1.7.0_80-b15)
# Java VM: Java HotSpot(TM) Client VM (24.80-b11 mixed mode, sharing windows-x86 )
# Problematic frame:
# V [jvm.dll+0xc6515]
及以下我显示了 hs_err_pidXXX.log 文件中的 10 个顶部堆栈帧:
Stack: [0x02150000,0x021a0000], sp=0x0219f49c, free space=317k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0xc6515]
V [jvm.dll+0xc66c9]
C [JniTestCpp.dll+0x13d52] JNIEnv_::GetMethodID+0x42
C [JniTestCpp.dll+0x14ecf] JniTestClass::callJavaStringFunction+0x3f
C [JniTestCpp.dll+0x16068] Java_jnitest_DllFunctions_doubleStringFunctionCall+0x28
j jnitest.DllFunctions.doubleStringFunctionCall()V+0
j jnitest.JniTestJava.main([Ljava/lang/String;)V+38
v ~StubRoutines::call_stub
V [jvm.dll+0x1429aa]
V [jvm.dll+0x20743e]
令我惊讶的是,如果我在 JniTestCpp.cpp 中不将 JniTestClass jniTestClass
声明为静态对象,而是声明它并调用 setup()
在如下所示的每个方法中,它不会崩溃,但会产生预期的结果。另外,我必须说我在调用 doubleIntFunctionCall();
而不是 doubleStringFunctionCall();
时工作是相当奇怪的
JniTestCpp.cpp - 这不会崩溃
#include "JniTestCpp.h"
#include "JniTestClass.h"
extern "C"
{
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaIntFunction();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaStringFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaStringFunction();
jniTestClass.callJavaStringFunction();
}
}
抱歉这么久 post,但这是我觉得能够明确提出我的问题的唯一方式。
更新
在函数 void JniTestClass::callJavaStringFunction()
中,如果我将其更改为以下内容:
void JniTestClass::callJavaStringFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
jstring j_string = env->NewStringUTF("String!");
env->CallVoidMethod(myObject, myMethod, j_string);
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->DeleteLocalRef(j_string);
}
我现在在使用 NewStringUTF()
创建的 jstring
上调用 DeleteLocalRef()
,程序仍然崩溃但打印出此异常消息:
java.lang.NoSuchMethodError: javaStringFunction
您的代码中有几处错误。
jobject myObject
和 jclass myClass
在 JNI 调用中重复使用。
在 JNI 方法中创建的所有 jobjects
默认情况下都是本地引用。每当一个 JNI 方法 returns 时,所有本地引用都会自动释放。
如果要跨方法调用重用jobject
(或jclass
也是对象引用),应使用NewGlobalRef. When a global reference is no longer needed, it should be deleted by DeleteGlobalRef将其转换为全局引用,否则引用对象永远不会被垃圾收集。
JNIEnv*
已缓存。
一般来说,JNIEnv*
永远不应存储以供日后重用。相反,您应该使用 JNIEnv*
作为每个 JNI 函数的第一个参数。或者,它可以通过 GetEnv 调用获得。请注意,每个线程都有自己的 JNIEnv*
,不适用于其他线程。
因此,根据评论员的要求,我终于找到了一个重现我的错误的 MCVE。所以一般的设置是 Java 使用 JNI 调用一个 dll,dll 抓住 运行 JVM 并存储一个指向 JNIEnv 的指针,它用来调用 java class(从 C++ 调用的 java class 不一定是原始调用 java 对象,这就是输入 jobject 不用于回调的原因) .在我进一步解释之前,让我 post 所有代码:
JniTest.java
package jnitest;
public class JniTestJava {
public static void main(String[] args) {
try {
System.load("<path-to-dll>");
} catch (Throwable e) {
e.printStackTrace();
}
DllFunctions dllFunctions = new DllFunctions();
dllFunctions.setup();
dllFunctions.singleIntFunctionCall();
dllFunctions.doubleIntFunctionCall();
dllFunctions.singleStringFunctionCall();
dllFunctions.doubleStringFunctionCall();
}
public void javaStringFunction(String input){
System.out.println(input);
}
public void javaIntFunction(int input){
System.out.println(input);
}
}
DllFunctions.java
package jnitest;
public class DllFunctions{
public native void singleIntFunctionCall();
public native void doubleIntFunctionCall();
public native void singleStringFunctionCall();
public native void doubleStringFunctionCall();
public native void setup();
}
JniTestCpp.h
#include <jni.h>
#ifndef _Included_jnitest_JniTestJava
#define _Included_jnitest_JniTestJava
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject);
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject);
#ifdef __cplusplus
}
#endif
#endif
JniTestCpp.cpp
#include "JniTestCpp.h"
#include "JniTestClass.h"
JniTestClass jniTestClass;
extern "C"
{
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
jniTestClass.setup();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaIntFunction();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaStringFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
jniTestClass.callJavaStringFunction();
jniTestClass.callJavaStringFunction();
}
}
JniTestClass.h
#include <jni.h>
class JniTestClass {
typedef jint(JNICALL * GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
public:
void setup();
void callJavaStringFunction();
void callJavaIntFunction();
void throwException(jthrowable ex);
private:
jobject myObject;
jclass myClass;
JNIEnv* env;
};
JniTestClass.cpp
#include "JniTestClass.h"
#include <Windows.h>
#include <fstream>
void JniTestClass::setup() {
jint jni_version = JNI_VERSION_1_4;
GetCreatedJavaVMs jni_GetCreatedJavaVMs;
jsize nVMs = 0;
jni_GetCreatedJavaVMs = (GetCreatedJavaVMs) GetProcAddress(GetModuleHandle(
TEXT("jvm.dll")), "JNI_GetCreatedJavaVMs");
jni_GetCreatedJavaVMs(NULL, 0, &nVMs);
JavaVM** buffer = new JavaVM*[nVMs];
jni_GetCreatedJavaVMs(buffer, nVMs, &nVMs);
buffer[0]->GetEnv((void **) &env, jni_version);
delete buffer;
myClass = env->FindClass("jnitest/JniTestJava");
myObject = env->NewObject(myClass, env->GetMethodID(myClass, "<init>", "()V"));
}
void JniTestClass::callJavaStringFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->CallVoidMethod(myObject, myMethod, env->NewStringUTF("String!"));
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
}
void JniTestClass::callJavaIntFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaIntFunction", "(I)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->CallVoidMethod(myObject, myMethod, 1);
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
}
void JniTestClass::throwException(jthrowable ex) {
env->ExceptionClear();
jclass clazz = env->GetObjectClass(ex);
jmethodID getMessage = env->GetMethodID(clazz,
"toString",
"()Ljava/lang/String;");
jstring message = (jstring) env->CallObjectMethod(ex, getMessage);
const char *mstr = env->GetStringUTFChars(message, NULL);
printf("%s \n", mstr);
throw std::runtime_error(mstr);
}
这里的意图是 JniTestCpp 应该只有 JNI 导出函数并且没有声明 classes。 JniTestClass 背后的想法是它应该保存所有 JNI 指针和变量(对象、class 和环境指针)并提供 JniTestCpp 可以使用的方法。
现在,此代码的显示方式在 JniTest.java 中的 dllFunctions.doubleStringFunctionCall();
调用时崩溃,输出如下:
1
1
1
String!
#
# A fatal error has been detected by the Java Runtime Environment:
#
# EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x6e306515, pid=1268, tid=8028
#
# JRE version: Java(TM) SE Runtime Environment (7.0_80-b15) (build 1.7.0_80-b15)
# Java VM: Java HotSpot(TM) Client VM (24.80-b11 mixed mode, sharing windows-x86 )
# Problematic frame:
# V [jvm.dll+0xc6515]
及以下我显示了 hs_err_pidXXX.log 文件中的 10 个顶部堆栈帧:
Stack: [0x02150000,0x021a0000], sp=0x0219f49c, free space=317k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [jvm.dll+0xc6515]
V [jvm.dll+0xc66c9]
C [JniTestCpp.dll+0x13d52] JNIEnv_::GetMethodID+0x42
C [JniTestCpp.dll+0x14ecf] JniTestClass::callJavaStringFunction+0x3f
C [JniTestCpp.dll+0x16068] Java_jnitest_DllFunctions_doubleStringFunctionCall+0x28
j jnitest.DllFunctions.doubleStringFunctionCall()V+0
j jnitest.JniTestJava.main([Ljava/lang/String;)V+38
v ~StubRoutines::call_stub
V [jvm.dll+0x1429aa]
V [jvm.dll+0x20743e]
令我惊讶的是,如果我在 JniTestCpp.cpp 中不将 JniTestClass jniTestClass
声明为静态对象,而是声明它并调用 setup()
在如下所示的每个方法中,它不会崩溃,但会产生预期的结果。另外,我必须说我在调用 doubleIntFunctionCall();
而不是 doubleStringFunctionCall();
JniTestCpp.cpp - 这不会崩溃
#include "JniTestCpp.h"
#include "JniTestClass.h"
extern "C"
{
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_setup(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleIntFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleIntFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaIntFunction();
jniTestClass.callJavaIntFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_singleStringFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaStringFunction();
}
JNIEXPORT void JNICALL Java_jnitest_DllFunctions_doubleStringFunctionCall(JNIEnv* java_env, jobject) {
JniTestClass jniTestClass;
jniTestClass.setup();
jniTestClass.callJavaStringFunction();
jniTestClass.callJavaStringFunction();
}
}
抱歉这么久 post,但这是我觉得能够明确提出我的问题的唯一方式。
更新
在函数 void JniTestClass::callJavaStringFunction()
中,如果我将其更改为以下内容:
void JniTestClass::callJavaStringFunction() {
jmethodID myMethod = env->GetMethodID(myClass, "javaStringFunction", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
jstring j_string = env->NewStringUTF("String!");
env->CallVoidMethod(myObject, myMethod, j_string);
if (env->ExceptionCheck()) {
throwException(env->ExceptionOccurred());
}
env->DeleteLocalRef(j_string);
}
我现在在使用 NewStringUTF()
创建的 jstring
上调用 DeleteLocalRef()
,程序仍然崩溃但打印出此异常消息:
java.lang.NoSuchMethodError: javaStringFunction
您的代码中有几处错误。
jobject myObject
和jclass myClass
在 JNI 调用中重复使用。在 JNI 方法中创建的所有
jobjects
默认情况下都是本地引用。每当一个 JNI 方法 returns 时,所有本地引用都会自动释放。如果要跨方法调用重用
jobject
(或jclass
也是对象引用),应使用NewGlobalRef. When a global reference is no longer needed, it should be deleted by DeleteGlobalRef将其转换为全局引用,否则引用对象永远不会被垃圾收集。JNIEnv*
已缓存。一般来说,
JNIEnv*
永远不应存储以供日后重用。相反,您应该使用JNIEnv*
作为每个 JNI 函数的第一个参数。或者,它可以通过 GetEnv 调用获得。请注意,每个线程都有自己的JNIEnv*
,不适用于其他线程。