问题是我们可以跨不同的JNI方法调用缓存jclass
和jmethodID
吗?
当尝试通过另一个JNI方法调用使用缓存的jclass
和jmethodID
创建某个特定类的对象时,我遇到了一些奇怪的行为。
这是一个简单的例子:
public class Main {
static {
System.loadLibrary("test-crash");
}
public static void main(String args[]) throws InterruptedException {
Thread.sleep(20000);
doAnotherAction(doSomeAction());
}
private static native long doSomeAction();
private static native void doAnotherAction(long ptr);
}
public class MyClass {
public int a;
public MyClass(int a) {
if(a == 10){
throw new IllegalArgumentException("a == 10");
}
this.a = a;
}
}
JNI函数所做的只是创建类MyClass
的对象。函数doSomeAction
返回一个指向缓存的jclass
和jmethodID
的指针。这是本机方法的实现:
struct test{
jclass mc;
jmethodID ctor;
};
JNIEXPORT jlong JNICALL Java_com_test_Main_doSomeAction
(JNIEnv *env, jclass jc){
(void) jc;
jclass mc = (*env)->FindClass(env, "com/test/MyClass");
jmethodID ctor = (*env)->GetMethodID(env, mc, "<init>", "(I)V");
struct test *test_ptr = malloc(sizeof *test_ptr);
test_ptr->mc = mc;
test_ptr->ctor = ctor;
printf("Creating element0\n");
jobject ae1 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
(void) ae1;
printf("Creating element0\n");
jobject ae2 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
(void) ae2;
printf("Creating element0\n");
jobject ae3 = (*env)->NewObject(env, test_ptr->mc, test_ptr->ctor, (jint) 0);
(void) ae3;
return (intptr_t) test_ptr;
}
JNIEXPORT void JNICALL Java_com_test_Main_doAnotherAction
(JNIEnv *env, jclass jc, jlong ptr){
(void) jc;
struct test *test_ptr= (struct test *) ptr;
jclass mc = test_ptr->mc;
jmethodID ctor = test_ptr->ctor;
printf("Creating element\n");
jobject ae1 = (*env)->NewObject(env, mc, ctor, (jint) 0);
(void) ae1;
printf("Creating element\n");
jobject ae2 = (*env)->NewObject(env, mc, ctor, (jint) 0);
(void) ae2;
printf("Creating element\n");
jobject ae3 = (*env)->NewObject(env, mc, ctor, (jint) 0); //CRASH!!
(void) ae3;
}
问题在于,尝试在0
中创建对象时,当解除引用Java_com_test_Main_doAnotherAction
时,程序崩溃。调用object_alloc
的{{1}}函数发生崩溃。
java_lang_Class::as_Klass(oopDesc*)
的沮丧是
java_lang_Class::as_Klass(oopDesc*)
Dump of assembler code for function _ZN15java_lang_Class8as_KlassEP7oopDesc:
0x00007f7f6b02eeb0 <+0>: movsxd rax,DWORD PTR [rip+0x932ab5] # 0x7f7f6b96196c <_ZN15java_lang_Class13_klass_offsetE>
0x00007f7f6b02eeb7 <+7>: push rbp
0x00007f7f6b02eeb8 <+8>: mov rbp,rsp
0x00007f7f6b02eebb <+11>: pop rbp
0x00007f7f6b02eebc <+12>: mov rax,QWORD PTR [rdi+rax*1]
0x00007f7f6b02eec0 <+16>: ret
在这里似乎包含一个指向相关rdi
的指针。我注意到的是前5次没有发生崩溃:
Oop
崩溃案例是
rdi 0x7191eb228
导致rdi 0x7191eb718
被退回并崩溃。
在不同的0x0
函数中使用Oop
和jclass
时,jmethodID
损坏了什么?如果我使用在本地找到的JNI
和jclass
创建对象,一切正常。
UPD:分析核心转储后,我发现rdi的加载方式为
jmethodID
虽然mov rdi,r13
#...
mov rdi,QWORD PTR [rdi]
在我的JNI函数中似乎没有更新...
答案 0 :(得分:5)
在JNI调用中缓存jclass
是一个重大(though typical)错误。
jclass
是jobject
的{{3}}-它是JNI引用,应加以管理。
按照JNI规范special case, JNI函数返回的所有Java对象都是本地引用。
因此,FindClass
返回一个本地JNI引用,该本地JNI引用在本机方法返回后立即变得无效。也就是说,如果移动了对象,GC将不会更新引用,否则另一个JNI调用可能会将同一插槽重新用于其他JNI引用。
为了在JNI调用之间缓存jclass
,您可以使用says函数将其转换为全局引用。
jthread
,jstring
,jarray
是jobjects
的其他示例,也应对其进行管理。
JNIEnv*
也不得缓存,因为它仅NewGlobalRef
有效。
同时jmethodID
和jfieldID
可以在JNI调用之间安全地重用-它们明确标识JVM中的方法/字段,并且只要持有者类为in the current thread活。但是,如果holder类恰好被垃圾回收,它们也可能变得无效。