NDK(21)JNI的5大正确性缺陷及优化技巧(注意是正确性缺陷)

  

  转自 : http://www.ibm.com/developerworks/cn/java/j-jni/index.html

 

JNI 编程缺陷可以分为两类:

  • 性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
  • 正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。

正确性缺陷

  5 大 JNI 正确性缺陷包括:

1.使用错误的 JNIEnv

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

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

正确性技巧 #1
仅在相关的单一线程中使用 JNIEnv。

2.未正确使用全局引用

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

lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
   jobject gref = (*env)->NewGlobalRef(env, keepObj);
}

  创建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。

正确性技巧 #6
始终跟踪全局引用,并确保不再需要对象时删除它们。

3.未检测异常

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

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

清单 10. 未能检测异常
1     jclass objectClass;
2     jfieldID fieldID;
3     jchar result = 0;
4 
5     objectClass = (*env)->GetObjectClass(env, obj);
6     fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
7     result = (*env)->GetCharField(env, obj, fieldID);

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

清单 11. 检测异常
 1     jclass objectClass;
 2     jfieldID fieldID;
 3     jchar result = 0;
 4 
 5     objectClass = (*env)->GetObjectClass(env, obj);
 6     fieldID = (*env)->GetFieldID(env, objectClass, "charField", "C");
 7     if ((*env)->ExceptionOccurred(env)) {
 8         return;
 9     }
10     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
在发起可能会导致异常的 JNI 调用后始终检测异常。

 4.未检测返回值

  许多 JNI 方法都通过返回值来指示调用成功与否。与未检测异常相似,这也存在一个缺陷,即代码未检测返回值却假定调用成功而继续运行。对于大多数 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() 不存在,则本机将造成程序崩溃。

正确性技巧 #3
始终检测 JNI 方法的返回值,并包括用于处理错误的代码路径。 

5.未正确使用数组方法

  GetXXXArrayElements() 和 ReleaseXXXArrayElements() 方法允许您请求任何元素。同样,GetPrimitiveArrayCritical()ReleasePrimitiveArrayCritical()GetStringCritical() 和 ReleaseStringCritical()允许您请求数组元素或字符串字节,以最大限度降低直接指向数组或字符串的可能性。这些方法的使用存在两个常见的缺陷。

  其一,忘记在ReleaseXXX() 方法调用中提供更改。即便使用 Critical 版本,也无法保证您能获得对数组或字符串的直接引用。一些 JVM 始终返回一个副本,并且在这些 JVM 中,如果您在 ReleaseXXX() 调用中指定了 JNI_ABORT,或者忘记调用了 ReleaseXXX(),则对数组的更改不会被复制回去。

  举例来说,考虑以下代码:

1     void modifyArrayWithoutRelease(JNIEnv* env, jobject obj, jarray arr1) {
2         jboolean isCopy;
3         jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
4         if ((*env)->ExceptionCheck(env)){
5             return;
6         }
7         buffer[0] = 1;
8     }

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

清单 12. 包括一个释放调用
 1 void modifyArrayWithRelease(JNIEnv* env, jobject obj, jarray arr1) {
 2     jboolean isCopy;
 3     jbyte* buffer = (*env)-> (*env)->GetByteArrayElements(env,arr1,&isCopy);
 4     if ((*env)->ExceptionCheck(env))
 5         return;
 6 
 7     buffer[0] = 1;
 8     (*env)->ReleaseByteArrayElements(env, arr1, buffer, JNI_COMMIT);
 9     if ((*env)->ExceptionCheck(env))
10         return;
11 }
正确性技巧 #4
不要忘记为每个 GetXXX() 使用模式 0(复制回去并释放内存)调用 ReleaseXXX()。

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

  举例来说,以下代码看上去可能没有问题

 1 void workOnPrimitiveArray(JNIEnv* env, jobject obj, jarray arr1) {
 2     jboolean isCopy;
 3     jbyte* buffer = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy);
 4     if ((*env)->ExceptionCheck(env))
 5         return;
 6 
 7     processBufferHelper(buffer);
 8     (*env)->ReleasePrimitiveArrayCritical(env, arr1, buffer, 0);
 9     if ((*env)->ExceptionCheck(env))
10         return;
11 }

  但是,我们需要验证在调用 processBufferHelper() 时可以运行的所有代码都没有违反任何限制。这些限制适用于在 Get 和 Release 调用之间执行的所有代码,无论它是不是本机的一部分。

正确性技巧 #5
确保代码不会在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间发起任何 JNI 调用或由于任何原因出现阻塞。

 

 

posted @ 2015-08-23 19:18  f9q  阅读(790)  评论(0编辑  收藏  举报