您的位置:首页 > 编程语言 > Java开发

使用 Java Native Interface 的最佳实践2

2013-01-10 14:45 453 查看
2.正确性缺陷

5 大 JNI正确性缺陷包括:

· 使用错误的JNIEnv

· 未检测异常

· 未检测返回值

· 未正确使用数组方法

· 未正确使用全局引用

2.1使用错误的JNIEnv

执行本机代码的线程使用JNIEnv发起 JNI方法调用。但是,JNIEnv并不是仅仅用于分派所请求的方法。JNI规范规定每个JNIEnv对于线程来说都是本地的。JVM可以依赖于这一假设,将额外的线程本地信息存储在JNIEnv中。一个线程使用另一个线程中的JNIEnv会导致一些小 bug 和难以调试的崩溃问题。

线程可以调用通过JavaVM对象使用 JNI调用接口的GetEnv()来获取JNIEnv。JavaVM对象本身可以通过使用JNIEnv方法调用 JNIGetJavaVM()来获取,并且可以被缓存以及跨线程共享。缓存JavaVM对象的副本将允许任何能访问缓存对象的线程在必要时获取对它自己的JNIEnv访问。要实现最优性能,线程应该绕过JNIEnv,因为查找它有时会需要大量的工作。

2.2未检测异常

本机能调用的许多 JNI方法都会引起与执行线程相关的异常。当 Java代码执行时,这些异常会造成执行流程发生变化,这样便会自动调用异常处理代码。当某个本机方法调用某个 JNI方法时会出现异常,但检测异常并采用适当措施的工作将由本机来完成。一个常见的 JNI缺陷是调用 JNI 方法而未在调用完成后测试异常。这会造成代码有大量漏洞以及程序崩溃。

举例来说,考虑调用GetFieldID()的代码,如果无法找到所请求的字段,则会出现NoSuchFieldError。如果本机代码继续运行而未检测异常,并使用它认为应该返回的字段 ID,则会造成程序崩溃。举例来说,如果 Java类经过修改,导致charField字段不再存在,则清单 10中的代码可能会造成程序崩溃 —而不是抛出一个NoSuchFieldError:
清单 10. 未能检测异常

jclass objectClass;

jfieldID fieldID;

jchar result = 0;

objectClass =(*env)->GetObjectClass(env, obj);

fieldID = (*env)->GetFieldID(env,objectClass, "charField", "C");
result = (*env)->GetCharField(env, obj, fieldID);

添加异常检测代码要比在事后尝试调试崩溃简单很多。经常,您只需要检测是否出现了某个异常,如果是则立即返回 Java代码以便抛出异常。然后,使用常规的 Java异常处理流程处理它或者显示它。举例来说,清单 11将检测异常:
清单 11. 检测异常

jclass objectClass;

jfieldID fieldID;

jchar result = 0;

objectClass =(*env)->GetObjectClass(env, obj);

fieldID = (*env)->GetFieldID(env,objectClass, "charField", "C");

if((*env)->ExceptionOccurred(env)) {

return;

}
result = (*env)->GetCharField(env, obj, fieldID);

不检测和清除异常会导致出现意外行为。您可以确定以下代码的问题吗?

fieldID = (*env)->GetFieldID(env,objectClass, "charField", "C");

if (fieldID == NULL){

fieldID =(*env)->GetFieldID(env, objectClass,"charField", "D");

}
return (*env)->GetIntField(env, obj, fieldID);

问题在于,尽管代码处理了初始GetFieldID()未返回字段 ID的情况,但它并未清除此调用将设置的异常。因此,本机返回的结果会造成立即抛出一个异常。

2.3未检测返回值

许多 JNI方法都通过返回值来指示调用成功与否。与未检测异常相似,这也存在一个缺陷,即代码未检测返回值却假定调用成功而继续运行。对于大多数 JNI方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。

您可以确定以下代码的问题吗?

正确性技巧 #3

始终检测 JNI方法的返回值,并包括用于处理错误的代码路径。
clazz = (*env)->FindClass(env, "com/ibm/j9//HelloWorld");

method = (*env)->GetStaticMethodID(env, clazz, "main",

"([Ljava/lang/String;)V");

(*env)->CallStaticVoidMethod(env, clazz, method, NULL);
问题在于,如果未发现HelloWorld类,或者如果main()不存在,则本机将造成程序崩溃。

2.4未正确使用数组方法

GetXXXArrayElements()和ReleaseXXXArrayElements()方法允许您请求任何元素。同样,GetPrimitiveArrayCritical()、ReleasePrimitiveArrayCritical()、GetStringCritical()和ReleaseStringCritical()允许您请求数组元素或字符串字节,以最大限度降低直接指向数组或字符串的可能性。这些方法的使用存在两个常见的缺陷。其一,忘记在ReleaseXXX()方法调用中提供更改。即便使用Critical版本,也无法保证您能获得对数组或字符串的直接引用。一些 JVM 始终返回一个副本,并且在这些 JVM中,如果您在ReleaseXXX()调用中指定了JNI_ABORT,或者忘记调用了ReleaseXXX(),则对数组的更改不会被复制回去。

举例来说,考虑以下代码:
void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) {

jboolean isCopy;

jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);

if ((*env)->ExceptionCheck(env)) return;

buffer[0] = 1;

}
正确性技巧 #4

不要忘记为每个GetXXX()使用模式0(复制回去并释放内存)调用ReleaseXXX()。
在提供直接指向数组的指针的 JVM上,该数组将被更新;但是,在返回副本的JVM上则不是如此。这会造成您的代码在一些 JVM上能够正常运行,而在其他JVM上却会出错。您应该始终始终包括一个释放(release)调用,如清单 12所示:

清单 12.包括一个释放调用
void modifyArrayWithRelease(JNIEnv* env, jobject obj, jarray arr1) {

jboolean isCopy;

jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);

if ((*env)->ExceptionCheck(env)) return;

buffer[0] = 1;

(*env)->ReleaseByteArrayElements(env, arr1, buffer, JNI_COMMIT);

if ((*env)->ExceptionCheck(env)) return;

}
第二个缺陷是不注重规范对在GetXXXCritical()和ReleaseXXXCritical()之间执行的代码施加的限制。本机可能不会在这些方法之间发起任何调用,并且可能不会由于任何原因而阻塞。未重视这些限制会造成应用程序或 JVM 中出现间断性死锁。

举例来说,以下代码看上去可能没有问题:
void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) {

jboolean isCopy;

jbyte* buffer = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy);

if ((*env)->ExceptionCheck(env)) return;

processBufferHelper(buffer);

(*env)->ReleasePrimitiveArrayCritical(env, arr1, buffer, 0);

if ((*env)->ExceptionCheck(env)) return;

}
正确性技巧 #5

确保代码不会在GetXXXCritical()和ReleaseXXXCritical()调用之间发起任何 JNI 调用或由于任何原因出现阻塞。
但是,我们需要验证在调用processBufferHelper()时可以运行的所有代码都没有违反任何限制。这些限制适用于在Get和Release调用之间执行的所有代码,无论它是不是本机的一部分。

2.5未正确使用全局引用

本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {

jobject gref = (*env)->NewGlobalRef(env, keepObj);

}
正确性技巧 #6

始终跟踪全局引用,并确保不再需要对象时删除它们。
创建全局引用时,JVM会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 —因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: