St.*_*rio 1 java java-native-interface jvm jvm-hotspot
问题是我们可以缓存jclass并jmethodID跨不同的JNI方法调用吗?
尝试通过缓存jclass和jmethodID其他JNI方法调用创建某个特定类的对象时,遇到了一些奇怪的行为。
这是一个简单的示例:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
问题是0尝试在中创建对象时取消引用时程序崩溃Java_com_test_Main_doAnotherAction。在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
Run Code Online (Sandbox Code Playgroud)
rdi这里似乎包含一个指向相关的指针Oop。我注意到的是前5次未发生崩溃的情况:
rdi 0x7191eb228
Run Code Online (Sandbox Code Playgroud)
崩溃的情况是
rdi 0x7191eb718
Run Code Online (Sandbox Code Playgroud)
导致0x0退货并崩溃。
是什么让Oop使用时损坏jclass,并jmethodID在不同的JNI功能呢?如果我创建对象与本地发现jclass和jmethodID一切工作就好了。
UPD:分析核心转储后,我发现rdi的加载方式为
mov rdi,r13
#...
mov rdi,QWORD PTR [rdi]
Run Code Online (Sandbox Code Playgroud)
虽然r13似乎在我的JNI函数中没有更新...
jclass跨JNI调用进行缓存是一个主要(尽管很典型)错误。
jclass是一个特例的jobject-这是一个JNI参考,并应加以管理。
正如JNI规范所说,JNI函数返回的所有Java对象都是本地引用。因此,FindClass返回本地JNI引用,该引用将在本机方法返回后立即失效。也就是说,如果移动了对象,GC将不会更新引用,否则另一个JNI调用可能会将同一插槽重新用于其他JNI引用。
为了缓存jclassJNI调用,您可以使用NewGlobalRef函数将其转换为全局引用。
jthread,jstring,jarray是其他例子jobjects,他们也应加以管理。
JNIEnv*也不要缓存,因为它仅在当前线程中有效。
同时jmethodID,它们jfieldID可以在JNI调用之间安全地重用-它们明确标识JVM中的方法/字段,并且只要Holder类还活着,它们就可以重复使用。但是,如果holder类碰巧被垃圾回收,它们也可能变得无效。