在另一个 JNI 函数中使用时 Oop 被破坏
Oop gets corrupted when using in another JNI function
问题是我们可以在不同的 JNI 方法调用中缓存 jclass
和 jmethodID
吗?
当我试图从另一个 JNI 方法调用中使用缓存的 jclass
和 jmethodID
创建某个特定 class 的对象时,我遇到了一些奇怪的行为。
这是一个简单的例子:
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 函数所做的只是创建 class MyClass
的对象。函数 doSomeAction
return 指向缓存的 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;
}
问题是当试图在 Java_com_test_Main_doAnotherAction
中创建对象时取消引用 0
时程序崩溃。崩溃发生在 object_alloc
函数调用 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
这里好像包含了一个指向相关Oop
的指针。我注意到的是前 5 次没有发生崩溃:
rdi 0x7191eb228
崩溃案例是
rdi 0x7191eb718
导致 0x0
被 return 编辑并崩溃。
在不同的 JNI
函数中使用 jclass
和 jmethodID
时,Oop
会损坏什么?如果我使用本地找到的 jclass
和 jmethodID
创建对象,一切正常。
UPD:分析核心转储后,我发现 rdi 被加载为
mov rdi,r13
#...
mov rdi,QWORD PTR [rdi]
虽然 r13
在我的 JNI 函数中似乎没有更新...
跨 JNI 调用缓存 jclass
是一个重大的 () 错误。
jclass
是 jobject
的 special case - 它是一个 JNI 引用,应该被管理。
作为 JNI 规范 says,JNI 函数返回的所有 Java 对象都是本地引用。
因此,FindClass
returns 本地 JNI 引用在本地方法 returns 后立即失效。也就是说,如果对象被移动,GC 将不会更新引用,或者另一个 JNI 调用可能会为不同的 JNI 引用重用相同的槽。
为了跨 JNI 调用缓存 jclass
,您可以使用 NewGlobalRef
函数将其转换为全局引用。
jthread
、jstring
、jarray
是jobjects
的其他例子,也应该管理
JNIEnv*
也不能缓存,因为它只在in the current thread.
有效
同时 jmethodID
和 jfieldID
可以在 JNI 调用中安全地重用 - 它们在 JVM 中明确标识一个 method/field 并且 intended for repeated use 只要持有人 class 还活着。但是,如果持有者 class 恰好被垃圾回收,它们也可能会失效。
问题是我们可以在不同的 JNI 方法调用中缓存 jclass
和 jmethodID
吗?
当我试图从另一个 JNI 方法调用中使用缓存的 jclass
和 jmethodID
创建某个特定 class 的对象时,我遇到了一些奇怪的行为。
这是一个简单的例子:
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 函数所做的只是创建 class MyClass
的对象。函数 doSomeAction
return 指向缓存的 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;
}
问题是当试图在 Java_com_test_Main_doAnotherAction
中创建对象时取消引用 0
时程序崩溃。崩溃发生在 object_alloc
函数调用 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
这里好像包含了一个指向相关Oop
的指针。我注意到的是前 5 次没有发生崩溃:
rdi 0x7191eb228
崩溃案例是
rdi 0x7191eb718
导致 0x0
被 return 编辑并崩溃。
在不同的 JNI
函数中使用 jclass
和 jmethodID
时,Oop
会损坏什么?如果我使用本地找到的 jclass
和 jmethodID
创建对象,一切正常。
UPD:分析核心转储后,我发现 rdi 被加载为
mov rdi,r13
#...
mov rdi,QWORD PTR [rdi]
虽然 r13
在我的 JNI 函数中似乎没有更新...
跨 JNI 调用缓存 jclass
是一个重大的 (
jclass
是 jobject
的 special case - 它是一个 JNI 引用,应该被管理。
作为 JNI 规范 says,JNI 函数返回的所有 Java 对象都是本地引用。
因此,FindClass
returns 本地 JNI 引用在本地方法 returns 后立即失效。也就是说,如果对象被移动,GC 将不会更新引用,或者另一个 JNI 调用可能会为不同的 JNI 引用重用相同的槽。
为了跨 JNI 调用缓存 jclass
,您可以使用 NewGlobalRef
函数将其转换为全局引用。
jthread
、jstring
、jarray
是jobjects
的其他例子,也应该管理
JNIEnv*
也不能缓存,因为它只在in the current thread.
同时 jmethodID
和 jfieldID
可以在 JNI 调用中安全地重用 - 它们在 JVM 中明确标识一个 method/field 并且 intended for repeated use 只要持有人 class 还活着。但是,如果持有者 class 恰好被垃圾回收,它们也可能会失效。