使用Java Native Interface的最佳实践

来源:互联网 发布:宣传淘宝店铺文案 编辑:程序博客网 时间:2024/04/28 21:51

简介:本文摘抄了IBM的技术文档部分内容然后根据自己开发学习中遇到的一些问题加以修改和整理针对Android平台开发提出了本文的JNI最佳实践。

    Java 环境和语言对于应用程序开发来说是非常安全和高效的。但是,一些应用程序却需要执行纯 Java 程序无法完成的一些任务,比如:

  • 与旧有代码集成,避免重新编写。
  • 解决Java语言在手机等手持平台上的运行效率问题。
  • 实现可用类库中所缺少的功能。Android的底层驱动就必须要使用linux的基本硬件驱动。
  • 最好与使用 C/C++ 编写的代码集成,以充分发掘性能或其他与环境相关的系统特性。
  • 解决需要非 Java 代码的特殊情况。举例来说,核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。
  • 把其他语言(C/C++)的优势和Java结合使用


本文介绍 JNI 用户最常遇到的 一些编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。

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

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

在本文的JNI讨论中,我把所有的j**ID,jobject以及继承与jobject对象的类型都看作为一个句柄(一个整数值)。这样做得原因是我自己感觉这样更加容易理解这些类型的真正面目。

本文讨论的一些方法是基于C++的基本思想进行的,在C中无法实现,但是该思想是可以借鉴的,反正我们最终的目的是了解并解决问题。

一、性能缺陷

本人在使用JNI时经常遇见的5大性能缺陷如下:

  • 不缓存jmethodID、jfieldID 和jclass
  • 触发拷贝数组副本
  • 回访(回到jvm获取相数值)而不是直接传递参数
  • 错误认定本机代码与Java 代码之间的界限
  • 使用大量本地引用(Local Reference),而未通知JVM

1.不缓存jmethodID、jfieldID 和jclass

要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()、GetFieldID()、GetMethodId() 和GetStaticMethodID()。对于 GetFieldID()、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化(类一旦加载了其MethodID和FieldID不会改变)。但是,获取字段或方法的调用有时会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。

举例来说,清单1展示了调用静态方法所需的 JNI 代码:

清单 1. 使用JNI调用静态方法

int val = 1;jmethodID method = NULL;jclass cls = NULL;cls = env->FindClass("com/shediao/Test");if (env->ExceptionCheck()) {   return ERR_FIND_CLASS_FAILED;//发现异常时立即返回可以使调用该jni函数的java方法抛出异常}method = env->GetStaticMethodID(cls, "myStaticFun", "(I)V");if (env->ExceptionCheck()) {   return ERR_GET_STATIC_METHOD_FAILED;}env->CallStaticVoidMethod(cls, method,val);if (env->ExceptionCheck()) {   return ERR_CALL_STATIC_METHOD_FAILED;}

当我们每次希望调用方法时查找类和方法ID都会产生六个本机调用,而不是第一次缓存类和方法ID时需要的两个调用。

缓存会对您应用程序的运行时造成显著的影响。考虑下面两个版本的方法,它们的作用是相同的。清单2使用了缓存的字段ID:

清单 2. 使用缓存的字段 ID

jint sumValues2(JNIEnv* env, jobject obj, jobject allValues){   //a,b,c,d,e,f为缓存的jfieldID;这里采用了回访数据效率是不高的   jint avalue = env->GetIntField(allValues, a);   jint bvalue = env->GetIntField(allValues, b);   jint cvalue = env->GetIntField(allValues, c);   jint dvalue = env->GetIntField(allValues, d);   jint evalue = env->GetIntField(allValues, e);   jint fvalue = env->GetIntField(allValues, f);   return avalue + bvalue + cvalue + dvalue + evalue + fvalue;}

清单 3 没有使用缓存的字段 ID:

清单 3. 未缓存字段 ID

int sumValues2(JNIEnv* env, jobject obj, jobject allValues){   //这里没有对GetObject和GetFieldID进行异常判断是为了简化分析   jclass cls = env->GetObjectClass(allValues);   jfieldID a = env->GetFieldID(cls, "a", "I");   jfieldID b = env->GetFieldID(cls, "b", "I");   jfieldID c = env->GetFieldID(cls, "c", "I");   jfieldID d = env->GetFieldID(cls, "d", "I");   jfieldID e = env->GetFieldID(cls, "e", "I");   jfieldID f = env->GetFieldID(cls, "f", "I");   jint avalue = env->GetIntField(allValues, a);   jint bvalue = env->GetIntField(allValues, b);   jint cvalue = env->GetIntField(allValues, c);   jint dvalue = env->GetIntField(allValues, d);   jint evalue = env->GetIntField(allValues, e);   jint fvalue = env->GetIntField(allValues, f);   return avalue + bvalue + cvalue + dvalue + evalue + fvalue}


清单2用3,572 ms运行了 10,000,000 次。清单3用了 86,217 ms—多花了 24 倍的时间。

性能技巧 #1

查找并全局缓存常用的类、字段 ID 和方法 ID。

2.触发数组副本

JNI在Java 代码和本机代码之间提供了一个干净的接口。为了维持这种分离,数组将作为不透明的句柄(不可析取的指针)传递,并且本机代码必须回调 JVM 以便使用set和get调用操作数组元素。Java规范让JVM实现决定让这些调用提供对数组的直接访问,还是返回一个数组副本。举例来说,当数组经过优化而不需要连续存储时,JVM可以返回一个副本。

随后,这些调用可以复制被操作的元素。举例来说,如果您对含有 1,000 个元素的数组调用 GetLongArrayElements(),则会造成至少分配或复制 8,000 字节的数据(每个long 1,000 元素*8 字节)。当您随后使用 ReleaseLongArrayElements() 更新数组的内容时,需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的 GetPrimitiveArrayCritical(),规范仍然准许 JVM 创建完整数组的副本。

GetTypeArrayRegion() 和SetTypeArrayRegion() 方法允许您获取和更新数组的一部分,而不是整个数组。通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。

举例来说,考虑相同方法的两个版本,如清单 4 所示:

清单 4. 相同方法的两个版本

jlong getElement( JNIEnv* env, jobject obj,                     jlongArray arr_j,                     int element){   jboolean isCopy;   jlong result;    //下面获取了整个数组的拷贝   jlong* buffer_j = env->GetLongArrayElements(arr_j, &isCopy);   result = buffer_j[element];   env->ReleaseLongArrayElements(arr_j, buffer_j, 0);   return result;}jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j,                   int element){     jlong result;     //只获取某个区间的数组     env->GetLongArrayRegion(arr_j, element,1, &result);     return result;}

第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时,运行第一个方法 10,000,000 次用了 12,055 ms;而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间!

另一方面,如果您最终要获取数组中的所有元素,则使用GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单4 中这两个 getElement() 方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。

性能技巧 #2

获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的API 调用来避免复制整个数组。

性能技巧 #3

在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。

3.回访而不是传递参数

在调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变化不需要改变方法签名。但是,对于JNI来说,本机代码必须通过一个或多个 JNI 调用返回到 JVM 以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本机代码过渡到 Java 代码要比普通方法调用开销更大。因此,对于 JNI 来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。

考虑清单 5 中的两个方法,第二个方法假定我们缓存了字段 ID:

清单 5. 两个方法版本

int sumValues(JNIEnv* env, jobject obj, jint a, jint b,jint c, jint d, jint e, jint f){   return a + b + c + d + e + f;}int sumValues2(JNIEnv* env, jobject obj, jobject allValues){    //这里假定已经缓存了jfieldID字段   jint avalue = env->GetIntField(allValues, a);   jint bvalue = env->GetIntField(allValues, b);   jint cvalue = env->GetIntField(allValues, c);   jint dvalue = env->GetIntField(allValues, d);   jint evalue = env->GetIntField(allValues, e);   jint fvalue = env->GetIntField(allValues, f);      return avalue + bvalue + cvalue + dvalue + evalue + fvalue;}

sumValues2() 方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues() 避免了大量的 JNI 开销。

性能技巧 #4

如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。

4.错误认定本机代码与 Java 代码之间的界限

本机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从Java代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。

因此,在设计 Java 代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。

举例来说,如果我们希望使用 JNI 为某个串行端口提供接口,则可以构造两种不同的接口。第一个版本如清单 6 所示:

清单 6. 到串行端口的接口:版本 1

//配置串口参数,并把配置参数传给java                jobject initializeSerialPort(JNIEnv* env, jobject obj,  jstring comPortName);//设置一个8位的字节发送到串口,其配置保存在jobject中作为参数,jni函数要获取串口配置必需通过这个jobject回访才可以void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig,   jint whichBit,  jint bitValue);//获取一个8位的一个字节,其配置保存在jobject中作为参数,jni函数要获取串口配置必需通过这个jobject回访才可以jint getSerialPortBit(JNIEnv* env,jobject obj,jobject serialPortConfig, jint whichBit);//读取下一个字节,具体参数保存在jobject中,jni函数要回访才可以获取具体的串口参数值void readNextByte(JNIEnv* env, jobject obj);//发送下一个字节,具体参数保存在jobject中,jni函数要回访才可以获取具体的串口参数值void sendNextByte(JNIEnv* env, jobject obj);


在 清单6 中,串行端口的所有配置数据都存储在由 initializeSerialPort() 方法返回的 Java 对象中,并且将 Java 代码完全控制对硬件中各数据位的设置。清单6 所示版本的一些问题会造成其性能差于 清单7 中的版本:

清单 7. 到串行端口的接口:版本 2

jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName);void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig, jbyte byte);jbyte readSerialPortByte(JNIEnv* env, jobject obj,  jlong serialPortConfig);

性能技巧 #5

定义 Java 代码与本机代码之间的界限,最大限度地减少两者之间的互相调用。

最显著的一个问题就是,清单6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单7 将配置信息存储在一个本机结构中(比如,一个struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单7 的实现的性能将大大改善。

性能技巧 #6

构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。

5.使用大量本地引用而未通知 JVM

JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用 GetObjectArrayElement()时,将返回对数组中对象的本地引用。考虑 清单8 中的代码在运行一个很大的数组时会使用多少本地引用:

清单 8. 创建本地引用

void workOnArray(JNIEnv* env, jobject obj, jarray array){   jint i;   jint count = env->GetArrayLength(array);   for (i=0; i < count; i++) {      jobject element = env->GetObjectArrayElement(array, i);      if(env->ExceptionOccurred(env)) {         break;      }            /* do something with array element */   }}


每次调用 GetObjectArrayElement() 时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。

这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本机代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了,但一些方法在其生存期中却需要更多的本地引用。对于这种情况,您应该删除不再需要的引用,方法是使用 JNI DeleteLocalRef() 调用,或者通知 JVM 您将使用更多的本地引用。

清单 9 向 清单8 中的示例添加了一个 DeleteLocalRef() 调用,用于通知 JVM 本地引用已不再需要,以及将可同时存在的本地引用的数量限制为一个合理的数值,而与数组的大小无关:

清单 9. 添加 DeleteLocalRef() 

void workOnArray(JNIEnv* env, jobject obj, jarray array){   jint i;   jint count = env->GetArrayLength(array);   for (i=0; i < count; i++) {      jobject element = env->GetObjectArrayElement(array, i);      if(env->ExceptionOccurred(env)) {         break;      }            /* do something with array element */      env->DeleteLocalRef(element);   }}


性能技巧 #7

当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。

您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError。

性能技巧 #8

如果某本机代码将同时存在大量本地引用,则调用JNIEnsureLocalCapacity()方法通知 JVM 并允许它优化对本地引用的处理。

二、正确性缺陷

5大JNI 正确性缺陷包括:

  • 使用错误的JNIEnv
  • 未检测异常
  • 未检测返回值
  • 未正确使用数组方法
  • 未正确使用全局引用

1.使用错误的JNIEnv

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

正确性技巧 #1

仅在相关的单一线程中使用 JNIEnv。

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

2.未检测异常

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

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

清单 10. 未能检测异常

jclass objectClass = NULL;jfieldID fieldID = NULL;jchar result = 0;//没有异常处理,也没有对返回值进行判断objectClass = env->GetObjectClass(obj);fieldID = env->GetFieldID(objectClass, "charField", "C");result = env->GetCharField(obj, fieldID);

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

清单 11. 检测异常

jclass objectClass = NULL;jfieldID fieldID = NULL;jchar result = 0;objectClass = env->GetObjectClass(obj);fieldID = env->GetFieldID(objectClass, "charField", "C");if(env->ExceptionOccurred(env)) {   return;//直接退出让java抛出异常}result = env->GetCharField(obj, fieldID);


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

fieldID = env->GetFieldID(objectClass, "charField", "C");if (fieldID == NULL){   fieldID = env->GetFieldID(objectClass,"charField", "D");}return env->GetIntField(obj, fieldID);


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

正确性技巧 #2

在发起可能会导致异常的 JNI 调用后始终检测异常。

3.未检测返回值

(该节讲解不够充分,有待进行补充)

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

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

clazz = env->FindClass("com/ibm/j9//HelloWorld");method = env->GetStaticMethodID(clazz, "main",                   "([Ljava/lang/String;)V");env->CallStaticVoidMethod(clazz, method, NULL);


问题在于,如果未发现 HelloWorld 类,或者如果 main() 不存在,则本机将造成程序崩溃。

正确性技巧 #3

始终检测JNI方法的返回值,并包括用于处理错误的代码路径。

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->GetByteArrayElements(env,arr1,&isCopy);   if (env->ExceptionCheck(env))        return;       buffer[0] = 1;}

在提供直接指向数组的指针的 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(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(arr1, &isCopy);    if (env->ExceptionCheck(env)) return;       processBufferHelper(buffer);      env->ReleasePrimitiveArrayCritical(arr1, buffer, 0);    if (env->ExceptionCheck(env)) return;}

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

正确性技巧 #4

不要忘记为每个 GetXXX() 使用模式 0(复制回去并释放内存)调用 ReleaseXXX()。

正确性技巧 #5

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

5.未正确使用全局引用

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


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

正确性技巧 #6

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

三、解避免一些缺陷

假设您编写了一些新 JNI 代码,或者继承了别处的某些 JNI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?下面就提供了5种较为简单的方法来避免:

1.编写JNI规范并根据该规范验证新代码

维持规范的限制列表并审查相关代码与列表的遵从性是一个很好的实践,这可以通过手动或自动代码分析来完成。确保遵从性的工作可能会比调试由于违背限制而出现的细小和间断性故障轻松很多。下面提供了一个专门针对新开发代码(或对您来说是新的)的规范顺从性检查列表:

  • 验证 JNIEnv 仅与与之相关的线程使用。
  • 确认未在 GetXXXCritical() 的 ReleaseXXXCritical() 部分调用 JNI 方法。
  • 对于进入关键部分的方法,验证该方法未在释放前返回。
  • 验证在所有可能引起异常的 JNI 调用之前都检测了异常。
  • 确保所有 Get/Release 调用在各JNI方法中都是相匹配的。(在后面的章节会有更加好的方法解决该问题)
  • 传递给 JNI 方法的参数属于正确的类型。
  • JNI代码未读取超过数组结束部分之外的内容。(这个是有点难度的)
  • 传递给 JNI 方法的指针都是有效的。

根据这些规矩会生成一个JNI检测报告,该检测报告的所有结论并不一定都是代码中的错误。它们还包括一些针对代码的建议,您应该仔细阅读它们以确保代码功能正常。

避免犯错技巧 #1

生成JNI编程规范,按照该规范对新代码进行静态分析(很有可能是通过程序或脚本完成)生成JNI检测报告。

2.分析方法跟踪

生成对已调用本机方法以及这些本机方法发起的 JNI 回调的跟踪,这对确定大量常见缺陷的根源是非常有用的。可确定的问题包括:

  • 大量 GetFieldID() 和 GetMethodID() 调用 — 特别是,如果这些调用针对相同的字段和方法 — 表示字段和方法未被缓存。
  • GetTypeArrayElements() 调用实例(而非 GetTypeArrayRegion())有时表示存在不必要的复制。
  • 在 Java 代码与本机代码之前来回快速切换(由时间戳指示)有时表示 Java 代码与本机代码之间的界限有误,从而造成性能较差。
  • 每个本机函数调用后面都紧接着大量 GetFieldID() 调用,这种模式表示并未传递所需的参数,而是强制本机回访完成工作所需的数据。
  • 调用可能抛出异常的 JNI 方法之后缺少对 ExceptionOccurred() 或 ExceptionCheck() 的调用表示本机未正确检测异常。
  • GetXXX() 和 ReleaseXXX() 方法调用的数量不匹配表示缺少释放操作。
  • 在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间调用 JNI 方法表示未遵循规范施加的限制。
  • 如果调用 GetXXXCritical() 和 ReleaseXXXCritical() 之间相隔的时间较长,则表示未遵循 “不要阻塞调用” 规范所施加的限制。
  • NewGlobalRef() 和 DeleteGlobalRef() 调用之间出现严重失衡表示释放不再需要的引用时出现故障。

一些 JVM 实现提供了一种可用于生存方法跟踪的机制。您还可以通过各种外部工具来生成跟踪,比如探查器和代码覆盖工具。

避免犯错技巧 #2

通过jvm自带跟踪工具或者外带代码查看器追踪jni运行,分析是否有jni回调的相关问题。

3.使用 -verbose:jni 选项

绝大多数JVM厂家还提供了一个 -verbose:jni 选项。开启此选项还会让 JVM 针对使用过多本地引用而未通知 JVM 的情况发起警告。

虽然 -verbose:jni 和 -Xcheck:jni:trace 选项可帮助您方便地获取所需的信息,但手动审查此信息是一项艰巨的任务。一个不错的提议是,创建一些脚本或实用工具来处理由 JVM 生成的跟踪文件,并查看警告。

在有些平台jvm不是由开发人员开启,我们可以把相关测试代码在开发机器上执行并打开-verbose:jni选项也可以达到相关效果。

避免犯错技巧 #3

开启-verbost:jni选项。

4.生成日志转储

运行中的 Java 进程生成的转储包含大量关于 JVM 状态的信息。对于许多 JVM 来说,它们包括关于全局引用的信息。

通过生成前后转储,您可以确定是否创建了任何未正常释放的全局引用。

您可以在 UNIX® 环境中通过对 java 进程发起 kill -3 或 kill -QUIT 来请求转储。在 Windows® 上,使用 Ctrl+Break 组合键。

5.执行代码审查

代码审查经常可用于确定常见缺陷,并且可以在各种级别上完成。继承新代码时,快速扫描可以发现各种问题,从而避免稍后花费更多时间进行调试。在某些情况下,审查是确定缺陷实例(比如未检查返回值)的唯一方法。举例来说,此代码的问题可能可以通过代码审查轻松确定,但却很难通过调试来发现:

int calledALot(JNIEnv* env, jobject obj, jobject allValues){   jclass cls = env->GetObjectClass(env,allValues);    jfieldID a = env->GetFieldID(cls, "a", "I");   jfieldID b = env->GetFieldID(cls, "b", "I");   jfieldID c = env->GetFieldID(cls, "c", "I");   jfieldID d = env->GetFieldID(cls, "d", "I");   jfieldID e = env->GetFieldID(cls, "e", "I");   jfieldID f = env->GetFieldID(cls, "f", "I");}jclass getObjectClassHelper(jobject object){    /* use globally cached JNIEnv */   return cls = (*globalEnvStatic)->GetObjectClass(globalEnvStatic,allValues); }

代码审查可能会发现第一个方法未正确缓存字段 ID,尽管重复使用了相同的 ID,并且第二个方法所使用的 JNIEnv 并不在应该在的线程上。


避免犯错技巧 #4

代码审查在任何团队和项目都一个最佳实践方案。

四、相关编程手法

1.删除局部引用

 局部引用在Native代码显示释放非常重要。你可能会问,既然Java虚拟机会自动释放局部变量为什么还需要我在Native代码中显示释放呢?原因有以下几点:

1)、Java虚拟机默认为Native引用分配的局部引用数量是有限的,大部分的Java虚拟机实现默认分配16个局部引用。当然Java虚拟机也提供API(PushLocalFrame,EnsureLocalCapacity)让你申请更多的局部引用数量(Java虚拟机不保证你一定能申请到)。有限的资源当然要省着点用,否则将会被Java虚拟机无情抛弃(程序崩溃)。JNI编程中,实现Native代码时强烈建议调用PushLocalFrame,EnsureLocalCapacity来确保Java虚拟机为你准备好了局部变量空间。 
2)、如果你实现的Native函数是工具函数,会被频繁的调用。如果你在Native函数中没有显示删除局部引用,那么每次调用该函数Java虚拟机都会创建一个新的局部引用,造成局部引用过多。尤其是该函数在Native代码中被频繁调用,代码的控制权没有交还给Java虚拟机,所以Java虚拟机根本没有机会释放这些局部变量。退一步讲,就算该函数直接返回给Java虚拟机,也不能保证没有问题,我们不能假设Native函数返回Java虚拟机之后,Java虚拟机马上就会回收Native函数中创建的局部引用,依赖于Java虚拟机实现。所以我们在实现Native函数时一定要记着删除不必要的局部引用,否则你的程序就有潜在的风险,不知道什么时候就会爆发。 
3)、如果你Native函数根本就不返回。比如消息循环函数——死循环等待消息,处理消息。如果你不显示删除局部引用,很快将会造成Java虚拟机的局部引用内存溢出。

    在JNI中显示释放局部引用的函数为DeleteLocalRef,大家可以查看手册来了解调用方法。

在JDK1.2中为了方便管理局部引用,引入了三个函数——EnsureLocalCapacity、PushLocalFrame、PopLocalFrame。这里介绍一下PushLocalFrame和PushLocalFrame函数。这两个函数是成对使用的,先调用PushLocalFrame,然后创建局部引用,并对其进行处理,最后调用PushLocalFrame释放局部引用,这时Java虚拟机也可以对其指向的对象进行垃圾回收。可以用C语言的栈来理解这对JNI API,调用PushLocalFrame之后Native代码创建的所有局部引用全部入栈,当调用PopLocalFrame之后,入栈的局部引用除了需要返回的局部引用(PushLocalFrame和PopLocalFrame这对函数可以返回一个局部引用给外部)之外,全部出栈,Java虚拟机这时可以释放他们指向的对象。具体的用法可以参考手册。这两个函数使JNI的局部引用由于和C语言的局部变量用法类似,所以强烈推荐使用。

下面我介绍一种又C++ RAII思想实现的一种较好的删除局部引用的方法

template<typename T>class scoped_local_ref {public:    scoped_local_ref(JNIEnv* env, T localRef = NULL)    : mEnv(env), mLocalRef(localRef)    { }    ~scoped_local_ref() {  reset();  }    void reset(T localRef = NULL) {        if (mLocalRef != NULL) {            mEnv->DeleteLocalRef(mLocalRef);            mLocalRef = localRef;        }    }    T get() const { return mLocalRef; }private:    JNIEnv* mEnv;T mLocalRef;    scoped_local_ref(const scoped_local_ref&);    scoped_local_ref& operator=(const scoped_local_ref&);};scoped_local_ref<jclass> myclass = env->FindClass(“java/lang/String”);//这个本地应用在生命周期过后会自动删除。

编程手法 #1

利用C++的RAII思想解决删除Local Reference问题。

2.行配对调用语句

(未完成)

五、结束语

现在,您已经了解了这些JNI编程缺陷,以及一些用于在已有或新代码中确定它们的良好实践。坚持应用这些实践有助于提高 JNI 代码的正确率,并且您的应用程序可以实现所需的性能水平。


转载出处:http://m.blog.csdn.net/blog/shediaoxu/8217301

0 0
原创粉丝点击