用爬虫做网站,国外域名注册价格,wordpress 门户网站源码,长沙seo优化推广公司JNI 简介
JNI#xff0c;Java Native Interface#xff0c;是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native code#xff1b;在 native code 中嵌入 Java 虚拟机调用 Java 的代码。
它支持将 Java 代码与使用其他…JNI 简介
JNIJava Native Interface是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——在 Java 程序中调用 native code在 native code 中嵌入 Java 虚拟机调用 Java 的代码。
它支持将 Java 代码与使用其他编程语言编写的代码相集成。如果您希望利用已有的代码资源那么可以使用 JNI 作为您工具包中的关键组件 —— 比如在面向服务架构SOA和基于云的系统中。但是如果在使用时未注意某些事项则 JNI 会迅速导致应用程序性能低下且不稳定。本文将确定 10 大 JNI 编程缺陷提供避免这些缺陷的最佳实践并介绍可用于实现这些实践的工具。 JNI 编程在软件开发中运用广泛其优势可以归结为以下几点
利用 native code 的平台相关性在平台相关的编程中彰显优势。对 native code 的代码重用。native code 底层操作更加高效。
然而任何事物都具有两面性JNI 编程也同样如此。程序员在使用 JNI 时应当认识到 JNI 编程中如下的几点弊端扬长避短才可以写出更加完善、高性能的代码
从 Java 环境到 native code 的上下文切换耗时、低效。JNI 编程如果操作不当可能引起 Java 虚拟机的崩溃。JNI 编程如果操作不当可能引起内存泄漏。
有一个有趣的地方值得注意一些较年轻的语言如 PHP在它们的本机代码支持方面仍然在努力克服这些问题。
与旧有代码集成避免重新编写。实现可用类库中所缺少的功能。举例来说在 Java 语言中实现 ping 时您可能需要 Internet Control Message Protocol (ICMP) 功能但基本类库并未提供它。最好与使用 C/C 编写的代码集成以充分发掘性能或其他与环境相关的系统特性。解决需要非 Java 代码的特殊情况。举例来说核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。
本文介绍 JNI 用户最常遇到的编码和设计错误。其目标是帮助您认识到并避免它们以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧并展示如何有效地应用它们。
性能缺陷
不缓存方法 ID、字段 ID 和类
要访问 Java 对象的字段并调用它们的方法本机代码必须调用 FindClass()、GetFieldID()、GetMethodId() 和GetStaticMethodID()。对于 GetFieldID()、GetMethodID() 和GetStaticMethodID()为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是获取字段或方法的调用有时会需要在 JVM 中完成大量工作因为字段和方法可能是从超类中继承而来的这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的因此您只需要查找一次然后便可重复使用。同样查找类对象的开销也很大因此也应该缓存它们。
举例来说代码 1 展示了调用静态方法所需的 JNI 代码
代码 1. 使用 JNI 调用静态方法
int val1;
jmethodID method;
jclass cls;cls (*env)-FindClass(env, com/ibm/example/TestClass);
if ((*env)-ExceptionCheck(env)) {return ERR_FIND_CLASS_FAILED;
}
method (*env)-GetStaticMethodID(env, cls, setInfo, (I)V);
if ((*env)-ExceptionCheck(env)) {return ERR_GET_STATIC_METHOD_FAILED;
}
(*env)-CallStaticVoidMethod(env, cls, method,val);
if ((*env)-ExceptionCheck(env)) {return ERR_CALL_STATIC_METHOD_FAILED;
}
当我们每次希望调用方法时查找类和方法 ID 都会产生六个本机调用而不是第一次缓存类和方法 ID 时需要的两个调用。
缓存会对您应用程序的运行时造成显著的影响。考虑下面两个版本的方法它们的作用是相同的。代码 2 使用了缓存的字段 ID代码 2. 使用缓存的字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){jint avalue (*env)-GetIntField(env, allValues, a);jint bvalue (*env)-GetIntField(env, allValues, b);jint cvalue (*env)-GetIntField(env, allValues, c);jint dvalue (*env)-GetIntField(env, allValues, d);jint evalue (*env)-GetIntField(env, allValues, e);jint fvalue (*env)-GetIntField(env, allValues, f);return avalue bvalue cvalue dvalue evalue fvalue;
} 性能技巧一
获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 API 调用来避免复制整个数组。
GetTypeArrayRegion() 和 SetTypeArrayRegion() 方法允许您获取和更新数组的一部分而不是整个数组。通过使用这些方法访问较大的数组您可以确保只复制本机代码将要实际使用的数组部分。
举例来说考虑相同方法的两个版本如代码 4 所示
代码 4. 相同方法的两个版本
jlong getElement(JNIEnv* env, jobject obj, jlongArray arr_j, int element){jboolean isCopy;jlong result;jlong* buffer_j (*env)-GetLongArrayElements(env, arr_j, isCopy);result buffer_j[element];(*env)-ReleaseLongArrayElements(env, arr_j, buffer_j, 0);return result;
}jlong getElement2(JNIEnv* env, jobject obj, jlongArray arr_j, int element){jlong result;(*env)-GetLongArrayRegion(env, arr_j, element,1, result);return result;
}
第一个版本可以生成两个完整的数组副本而第二个版本则完全没有复制数组。当数组大小为 1,000 字节时运行第一个方法 10,000,000 次用了 12,055 ms而第二个版本仅用了 1,421 ms。第一个版本多花了 8.5 倍的时间
性能技巧二 触发数组副本
查找并全局缓存常用的类、字段 ID 和方法 ID。
代码 3 没有使用缓存的字段 ID代码 3. 未缓存字段 ID
int sumValues2(JNIEnv* env, jobject obj, jobject allValues){jclass cls (*env)-GetObjectClass(env,allValues);jfieldID a (*env)-GetFieldID(env, cls, a, I);jfieldID b (*env)-GetFieldID(env, cls, b, I);jfieldID c (*env)-GetFieldID(env, cls, c, I);jfieldID d (*env)-GetFieldID(env, cls, d, I);jfieldID e (*env)-GetFieldID(env, cls, e, I);jfieldID f (*env)-GetFieldID(env, cls, f, I);jint avalue (*env)-GetIntField(env, allValues, a);jint bvalue (*env)-GetIntField(env, allValues, b);jint cvalue (*env)-GetIntField(env, allValues, c);jint dvalue (*env)-GetIntField(env, allValues, d);jint evalue (*env)-GetIntField(env, allValues, e);jint fvalue (*env)-GetIntField(env, allValues, f);return avalue bvalue cvalue dvalue evalue fvalue
}
代码2 3,572 ms 运行了 10,000,000 次。代码 3 用了 86,217 ms — 多花了 24 倍的时间。
JNI 在 Java 代码和本机代码之间提供了一个干净的接口。为了维持这种分离数组将作为不透明的句柄传递并且本机代码必须回调 JVM 以便使用 set 和 get 调用操作数组元素。Java 规范让 JVM 实现决定让这些调用提供对数组的直接访问还是返回一个数组副本。举例来说当数组经过优化而不需要连续存储时JVM 可以返回一个副本。参见参考资料 获取关于 JVM 的信息。
随后这些调用可以复制被操作的元素。举例来说如果您对含有 1,000 个元素的数组调用 GetLongArrayElements()则会造成至少分配或复制 8,000 字节的数据每个long 1,000 元素 * 8 字节。当您随后使用 ReleaseLongArrayElements() 更新数组的内容时需要另外复制 8,000 字节的数据来更新数组。即使您使用较新的GetPrimitiveArrayCritical()规范仍然准许 JVM 创建完整数组的副本。
性能技巧三 回访而不是传递参数
在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容则不要逐个迭代数组中的元素。
另一方面如果您最终要获取数组中的所有元素则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素则清单 4代码4中这两个 getElement() 方法都不适用。比较好的方法是在一个调用中获取大小合理的数组部分然后再迭代所有这些元素重复操作直到覆盖整个数组。 在调用某个方法时您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中传递对象通常能提供较好的封装因为对象字段的变化不需要改变方法签名。但是对于 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){jint avalue (*env)-GetIntField(env, allValues, a);jint bvalue (*env)-GetIntField(env, allValues, b);jint cvalue (*env)-GetIntField(env, allValues, c);jint dvalue (*env)-GetIntField(env, allValues, d);jint evalue (*env)-GetIntField(env, allValues, e);jint fvalue (*env)-GetIntField(env, allValues, f);return avalue bvalue cvalue dvalue evalue fvalue;
} 性能技巧四 使用大量本地引用而未通知 JVM
构造应用程序的数据使它位于界限的正确的侧并且可以由使用它的代码访问而不需要大量跨界调用。
最显著的一个问题就是清单 6代码6清单 6 中的接口在设置或检索每个位以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是清单 6代码6清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7代码7清单 7 将配置信息存储在一个本机结构中比如一个struct并向 Java 代码返回了一个不透明的句柄该句柄可以在后续调用中返回。这意味着当本机代码正在运行时它可以直接访问该结构而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此使用清单 7代码7清单 7 的实现的性能将大大改善。JNI 函数返回的任何对象都会创建本地引用。举例来说当您调用 GetObjectArrayElement() 时将返回对数组中对象的本地引用。考虑代码 8 中的代码在运行一个很大的数组时会使用多少本地引用
代码 8. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array){jint i;jint count (*env)-GetArrayLength(env, array);for (i0; i count; i) {jobject element (*env)-GetObjectArrayElement(env, array, i);if((*env)-ExceptionOccurred(env)) {break;}/* do something with array element */}
}
每次调用 GetObjectArrayElement() 时都会为元素创建一个本地引用并且直到本机代码运行完成时才会释放。数组越大所创建的本地引用就越多。
性能技巧五 错误认定本机代码与 Java 代码之间的界限
如果可能将各参数传递给 JNI 本机代码以便本机代码回调 JVM 获取所需的数据。
sumValues2() 方法需要 6 个 JNI 回调并且运行 10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢 6 倍后者只需要 596 ms。通过传递 JNI 方法所需的数据sumValues() 避免了大量的 JNI 开销。
本机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外这种越界操作会干扰 JVM 优化代码执行的能力。举例来说随着 Java 代码与本机代码之间互操作的增加实时编译器的效率会随之降低。经过测量我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样从本机代码中调用 Java 代码也需要耗费大量的时间。
性能技巧六
定义 Java 代码与本机代码之间的界限最大限度地减少两者之间的互相调用。
因此在设计 Java 代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于 Java/本机界限的正确一侧。如果数据未在正确的一侧则另一侧访问数据的需求则会持续发起越界调用。
举例来说如果我们希望使用 JNI 为某个串行端口提供接口则可以构造两种不同的接口。第一个版本如代码 6 所示
代码 6. 到串行端口的接口版本 1
/*** Initializes the serial port and returns a java SerialPortConfig objects* that contains the hardware address for the serial port, and holds* information needed by the serial port such as the next buffer * to write data into* * param env JNI env that can be used by the method* param comPortName the name of the serial port* returns SerialPortConfig object to be passed ot setSerialPortBit * and getSerialPortBit calls*/
jobject initializeSerialPort(JNIEnv* env, jobject obj, jstring comPortName);/*** Sets a single bit in an 8 bit byte to be sent by the serial port** param env JNI env that can be used by the method* param serialPortConfig object returned by initializeSerialPort* param whichBit value from 1-8 indicating which bit to set* param bitValue 0th bit contains bit value to be set */
void setSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit, jint bitValue);/*** Gets a single bit in an 8 bit byte read from the serial port** param env JNI env that can be used by the method* param serialPortConfig object returned by initializeSerialPort* param whichBit value from 1-8 indicating which bit to read* returns the bit read in the 0th bit of the jint */
jint getSerialPortBit(JNIEnv* env, jobject obj, jobject serialPortConfig, jint whichBit);/*** Read the next byte from the serial port* * param env JNI env that can be used by the method*/
void readNextByte(JNIEnv* env, jobject obj);/*** Send the next byte** param env JNI env that can be used by the method*/
void sendNextByte(JNIEnv* env, jobject obj);
在代码6中串行端口的所有配置数据都存储在由 initializeSerialPort() 方法返回的 Java 对象中并且将 Java 代码完全控制对硬件中各数据位的设置。清单 6代码6清单 6 所示版本的一些问题会造成其性能差于代码 7 中的版本
代码 7. 到串行端口的接口版本 2
/*** Initializes the serial port and returns an opaque handle to a native* structure that contains the hardware address for the serial port * and holds information needed by the serial port such as * the next buffer to write data into** param env JNI env that can be used by the method* param comPortName the name of the serial port* returns opaque handle to be passed to setSerialPortByte and * getSerialPortByte calls */
jlong initializeSerialPort2(JNIEnv* env, jobject obj, jstring comPortName);/*** sends a byte on the serial port* * param env JNI env that can be used by the method* param serialPortConfig opaque handle for the serial port* param byte the byte to be sent*/
void sendSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig, jbyte byte);/*** Reads the next byte from the serial port* * param env JNI env that can be used by the method* param serialPortConfig opaque handle for the serial port* returns the byte read from the serial port*/
jbyte readSerialPortByte(JNIEnv* env, jobject obj, jlong serialPortConfig); 性能技巧七
如果某本机代码将同时存在大量本地引用则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。
您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下则未成功通知 JVM 会导致FatalError。
性能技巧八
当本机代码造成创建大量本地引用时在各引用不再需要时删除它们。
这些本地引用会在本机方法终止时自动释放。JNI 规范要求各本机代码至少能创建 16 个本地引用。虽然这对许多方法来说都已经足够了但一些方法在其生存期中却需要更多的本地引用。对于这种情况您应该删除不再需要的引用方法是使用 JNIDeleteLocalRef() 调用或者通知 JVM 您将使用更多的本地引用。
代码 9 向 代码8 中的示例添加了一个 DeleteLocalRef() 调用用于通知 JVM 本地引用已不再需要以及将可同时存在的本地引用的数量限制为一个合理的数值而与数组的大小无关
代码 9. 添加 DeleteLocalRef()
void workOnArray(JNIEnv* env, jobject obj, jarray array){jint i;jint count (*env)-GetArrayLength(env, array);for (i0; i count; i) {jobject element (*env)-GetObjectArrayElement(env, array, i);if((*env)-ExceptionOccurred(env)) {break;}/* do something with array element */(*env)-DeleteLocalRef(env, element);}
} 正确性缺陷
5 大 JNI 正确性缺陷包括
使用错误的 JNIEnv
执行本机代码的线程使用 JNIEnv 发起 JNI 方法调用。但是JNIEnv 并不是仅仅用于分派所请求的方法。JNI 规范规定每个JNIEnv 对于线程来说都是本地的。JVM 可以依赖于这一假设将额外的线程本地信息存储在 JNIEnv 中。一个线程使用另一个线程中的JNIEnv 会导致一些小 bug 和难以调试的崩溃问题。 仅在相关的单一线程中使用 JNIEnv。
线程可以调用通过 JavaVM 对象使用 JNI 调用接口的 GetEnv() 来获取 JNIEnv。JavaVM 对象本身可以通过使用JNIEnv 方法调用 JNI GetJavaVM() 来获取并且可以被缓存以及跨线程共享。缓存 JavaVM 对象的副本将允许任何能访问缓存对象的线程在必要时获取对它自己的 JNIEnv 访问。要实现最优性能线程应该绕过JNIEnv因为查找它有时会需要大量的工作。
未检测异常
本机能调用的许多 JNI 方法都会引起与执行线程相关的异常。当 Java 代码执行时这些异常会造成执行流程发生变化这样便会自动调用异常处理代码。当某个本机方法调用某个 JNI 方法时会出现异常但检测异常并采用适当措施的工作将由本机来完成。一个常见的 JNI 缺陷是调用 JNI 方法而未在调用完成后测试异常。这会造成代码有大量漏洞以及程序崩溃。
举例来说考虑调用 GetFieldID() 的代码如果无法找到所请求的字段则会出现 NoSuchFieldError。如果本机代码继续运行而未检测异常并使用它认为应该返回的字段 ID则会造成程序崩溃。举例来说如果 Java 类经过修改导致charField 字段不再存在则代码 10 中的代码可能会造成程序崩溃 — 而不是抛出一个 NoSuchFieldError
代码 10. 未能检测异常在发起可能会导致异常的 JNI 调用后始终检测异常。
添加异常检测代码要比在事后尝试调试崩溃简单很多。经常您只需要检测是否出现了某个异常如果是则立即返回 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 的情况但它并未清除 此调用将设置的异常。因此本机返回的结果会造成立即抛出一个异常。
未检测返回值
许多 JNI 方法都通过返回值来指示调用成功与否。与未检测异常相似这也存在一个缺陷即代码未检测返回值却假定调用成功而继续运行。对于大多数 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() 不存在则本机将造成程序崩溃。
未正确使用数组方法
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;
}
其他正确性技巧一
不要忘记为每个 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;
}
其他正确性技巧二 未正确使用全局引用
确保代码不会在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间发起任何 JNI 调用或由于任何原因出现阻塞。
但是我们需要验证在调用 processBufferHelper() 时可以运行的所有代码都没有违反任何限制。这些限制适用于在 Get 和 Release 调用之间执行的所有代码无论它是不是本机的一部分。 本机可以创建一些全局引用以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用或者完全失去对它们的跟踪。考虑一个本机创建了全局引用但是未删除它或将它存储在某处
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {jobject gref (*env)-NewGlobalRef(env, keepObj);
}
其他正确性技巧三
始终跟踪全局引用并确保不再需要对象时删除它们。
创建全局引用时JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时它不仅会释放全局引用应用程序还无法获取引用以便稍后释放它 — 因此对象将会始终存在。不释放全局引用会造成各种问题不仅因为它们会保持对象本身为活动状态还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下这会显著加剧内存泄漏。
JAVA 中的内存泄漏
JAVA 编程中的内存泄漏从泄漏的内存位置角度可以分为两种JVM 中 Java Heap 的内存泄漏JVM 内存中 native memory 的内存泄漏。
Java Heap 的内存泄漏
Java 对象存储在 JVM 进程空间中的 Java Heap 中Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多占据 Java Heap 的空间也越来越大JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限并且在 GC 后仍然没有足够空间分配新的 Java 对象便会抛出 out of memory 异常导致 JVM 进程崩溃。
Java Heap 中 out of memory 异常的出现有两种原因——①程序过于庞大致使过多 Java 对象的同时存在②程序编写的错误导致 Java Heap 内存泄漏。
多种原因可能导致 Java Heap 内存泄漏。JNI 编程错误也可能导致 Java Heap 的内存泄漏。
JVM 中 native memory 的内存泄漏
从操作系统角度看JVM 在运行时和其它进程没有本质区别。在系统级别上它们具有同样的调度机制同样的内存分配方式同样的内存格局。
JVM 进程空间中Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中例如载入的代码映像线程的堆栈线程的管理控制块JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。
在 JVM 运行中多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配占据越来越多 native memory 空间并且达到 native memory 上限时JVM 会抛出异常使 JVM 进程异常退出。而此时 Java Heap 往往还没有达到上限。
多种原因可能导致 JVM 的 native memory 内存泄漏。例如 JVM 在运行中过多的线程被创建并且在同时运行。JVM 为线程分配的资源就可能耗尽 native memory 的容量。
JNI 编程错误也可能导致 native memory 的内存泄漏。对这个话题的讨论是本文的重点。
JNI 编程中明显的内存泄漏
JNI 编程实现了 native code 和 Java 程序的交互因此 JNI 代码编程既遵循 native code 编程语言的编程规则同时也遵守 JNI 编程的文档规范。在内存管理方面native code 编程语言本身的内存管理机制依然要遵循同时也要考虑 JNI 编程的内存管理。
本章简单概括 JNI 编程中显而易见的内存泄漏。从 native code 编程语言自身的内存管理和 JNI 规范附加的内存管理两方面进行阐述。
Native Code 本身的内存泄漏
JNI 编程首先是一门具体的编程语言或者 C 语言或者 C或者汇编或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此JNI 程序开发者要遵循 native 语言本身的内存管理机制避免造成内存泄漏。以 C 语言为例当用 malloc() 在进程堆中动态分配内存时JNI 程序在使用完后应当调用 free() 将内存释放。总之所有在 native 语言编程中应当注意的内存泄漏规则在 JNI 编程中依然适应。
Native 语言本身引入的内存泄漏会造成 native memory 的内存严重情况下会造成 native memory 的 out of memory。
Global Reference 引入的内存泄漏
JNI 编程还要同时遵循 JNI 的规范标准JVM 附加了 JNI 编程特有的内存管理机制。
JNI 中的 Local Reference 只在 native method 执行时存在当 native method 执行完后自动失效。这种自动失效使得对 Local Reference 的使用相对简单native method 执行完后它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。
而 Global Reference 对 Java 对象的引用一直有效因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference务必确保在不用的时候删除。就像在 C 语言中调用 malloc() 动态分配一块内存之后调用 free() 释放一样。否则Global Reference 引用的 Java 对象将永远停留在 Java Heap 中造成 Java Heap 的内存泄漏。
JNI 编程中潜在的内存泄漏——对 LocalReference 的深入理解
Local Reference 在 native method 执行完成后会自动被释放似乎不会造成任何的内存泄漏。但这是错误的。对 Local Reference 的理解不够会造成潜在的内存泄漏。
本章重点阐述 Local Reference 使用不当可能引发的内存泄漏。引入两个错误实例也是 JNI 程序员容易忽视的错误在此基础上介绍 Local Reference 表对比 native method 中的局部变量和 JNI Local Reference 的不同使读者深入理解 JNI Local Reference 的实质最后为 JNI 程序员提出应该如何正确合理使用 JNI Local Reference以避免内存泄漏。
错误实例 1
在某些情况下我们可能需要在 native method 里面创建大量的 JNI Local Reference。这样可能导致 native memory 的内存泄漏如果在 native method 返回之前 native memory 已经被用光就会导致 native memory 的 out of memory。
在代码代码 1 里我们循环执行 count 次JNI function NewStringUTF() 在每次循环中从 Java Heap 中创建一个 String 对象str 是 Java Heap 传给 JNI native method 的 Local Reference每次循环中新创建的 String 对象覆盖上次循环中 str 的内容。str 似乎一直在引用到一个 String 对象。整个运行过程中我们看似只创建一个 Local Reference。
执行代码代码 1 的程序第一部分为 Java 代码nativeMethod(int i) 中输入参数设定循环的次数。第二部分为 JNI 代码用 C 语言实现了 nativeMethod(int i)。
代码 1. Local Reference 引发内存泄漏
Java 代码部分class TestLocalReference {private native void nativeMethod(int i);public static void main(String args[]) {TestLocalReference c new TestLocalReference();//call the jni native methodc.nativeMethod(1000000);} static {//load the jni librarySystem.loadLibrary(StaticMethodCall);}}JNI 代码nativeMethod(int i) 的 C 语言实现#includestdio.h#includejni.h#includeTestLocalReference.hJNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod(JNIEnv * env, jobject obj, jint count){jint i 0;jstring str;for(; icount; i)str (*env)-NewStringUTF(env, 0);}运行结果JVMCI161: FATAL ERROR in native method: Out of memory when expandinglocal ref table beyond capacityat TestLocalReference.nativeMethod(Native Method)at TestLocalReference.main(TestLocalReference.java:9)
运行结果证明JVM 运行异常终止原因是创建了过多的 Local Reference从而导致 out of memory。实际上nativeMethod 在运行中创建了越来越多的 JNI Local Reference而不是看似的始终只有一个。过多的 Local Reference导致了 JNI 内部的 JNI Local Reference 表内存溢出。
错误实例 2
实例 2 是实例 1 的变种Java 代码未作修改但是 nativeMethod(int i) 的 C 语言实现稍作修改。在 JNI 的 native method 中实现的 utility 函数中创建 Java 的 String 对象。utility 函数只建立一个 String 对象返回给调用函数但是 utility 函数对调用者的使用情况是未知的每个函数都可能调用它并且同一函数可能调用它多次。在实例 2 中nativeMethod 在循环中调用 count 次utility 函数在创建一个 String 对象后即返回并且会有一个退栈过程似乎所创建的 Local Reference 会在退栈时被删除掉所以应该不会有很多 Local Reference 被创建。实际运行结果并非如此。
代码 2. Local Reference 引发内存泄漏
Java 代码部分参考实例 1未做任何修改。JNI 代码nativeMethod(int i) 的 C 语言实现#includestdio.h#includejni.h#includeTestLocalReference.hjstring CreateStringUTF(JNIEnv * env){return (*env)-NewStringUTF(env, 0);}JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod(JNIEnv * env, jobject obj, jint count){jint i 0;for(; icount; i){str CreateStringUTF(env);}}
运行结果JVMCI161: FATAL ERROR in native method: Out of memory when expanding local reftable beyond capacityat TestLocalReference.nativeMethod(Native Method)at TestLocalReference.main(TestLocalReference.java:9)
运行结果证明实例 2 的结果与实例 1 的完全相同。过多的 Local Reference 被创建仍然导致了 JNI 内部的 JNI Local Reference 表内存溢出。实际上在 utility 函数 CreateStringUTF(JNIEnv * env)
执行完成后的退栈过程中创建的 Local Reference 并没有像 native code 中的局部变量那样被删除而是继续在 Local Reference 表中存在并且有效。Local Reference 和局部变量有着本质的区别。
Local Reference 深层解析
Java JNI 的文档规范只描述了 JNI Local Reference 是什么存在的目的以及应该怎么使用 Local Reference开放的接口规范。但是对 Java 虚拟机中 JNI Local Reference 的实现并没有约束不同的 Java 虚拟机有不同的实现机制。这样的好处是不依赖于具体的 JVM 实现有好的可移植性并且开发简单规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质“不知道为什么这样做”。对 Local Reference 没有深层的理解就会在编程过程中无意识的犯错。
Local Reference 和 Local Reference 表
理解 Local Reference 表的存在是理解 JNI Local Reference 的关键。
JNI Local Reference 的生命期是在 native method 的执行期从 Java 程序切换到 native code 环境时开始创建或者在 native method 执行时调用 JNI function 创建在 native method 执行完毕切换回 Java 程序时所有 JNI Local Reference 被删除生命期结束调用 JNI function 可以提前结束其生命期。
实际上每当线程从 Java 环境切换到 native code 上下文时J2NJVM 会分配一块内存创建一个 Local Reference 表这个表用来存放本次 native method 执行中创建的所有的 Local Reference。每当在 native code 中引用到一个 Java 对象时JVM 就会在这个表中创建一个 Local Reference。比如实例 1 中我们调用 NewStringUTF() 在 Java Heap 中创建一个 String 对象后在 Local Reference 表中就会相应新增一个 Local Reference。
Local Ref 不是 native code 的局部变量
很多人会误将 JNI 中的 Local Reference 理解为 Native Code 的局部变量。这是错误的。
Native Code 的局部变量和 Local Reference 是完全不同的区别可以总结为
⑴局部变量存储在线程堆栈中而 Local Reference 存储在 Local Ref 表中。
⑵局部变量在函数退栈后被删除而 Local Reference 在调用 DeleteLocalRef() 后才会从 Local Ref 表中删除并且失效或者在整个 Native Method 执行结束后被删除。
⑶可以在代码中直接访问局部变量而 Local Reference 的内容无法在代码中直接访问必须通过 JNI function 间接访问。JNI function 实现了对 Local Reference 的间接访问JNI function 的内部实现依赖于具体 JVM。
代码代码 1 中 str (*env)-NewStringUTF(env, 0);
str 是 jstring 类型的局部变量。Local Ref 表中会新创建一个 Local Reference引用到 NewStringUTF(env, 0) 在 Java Heap 中新建的 String 对象。
JNI 通过 str 和指针 p 间接定位 Local Ref3但 p 和 Local Ref3 对 JNI 程序员不可见。
Local Reference 导致内存泄漏
在以上论述基础上我们通过分析错误实例 1 和实例 2来分析 Local Reference 可能导致的内存泄漏加深对 Local Reference 的深层理解。
分析错误实例 1
局部变量 str 在每次循环中都被重新赋值间接指向最新创建的 Local Reference前面创建的 Local Reference 一直保留在 Local Ref 表中。
分析错误实例 2
实例 2 与实例 1 相似虽然每次循环中调用工具函数 CreateStringUTF(env) 来创建对象但是在 CreateStringUTF(env) 返回退栈过程中只是局部变量被删除而每次调用创建的 Local Reference 仍然存在 Local Ref 表中并且有效引用到每个新创建的 string 对象。str 局部变量在每次循环中被赋新值。
这样的内存泄漏是潜在的但是这样的错误在 JNI 程序员编程过程中却经常出现。通常情况在触发 out of memory 之前native method 已经执行完毕切换回 Java 环境所有 Local Reference 被删除问题也就没有显露出来。但是某些情况下就会引发 out of memory导致实例 1 和实例 2 中的 JVM 崩溃。
控制 Local Reference 生命期
因此在 JNI 编程时正确控制 JNI Local Reference 的生命期。如果需要创建过多的 Local Reference那么在对被引用的 Java 对象操作结束后需要调用 JNI function如 DeleteLocalRef()及时将 JNI Local Reference 从 Local Ref 表中删除以避免潜在的内存泄漏。 避免常见缺陷
假设您编写了一些新 JNI 代码或者继承了别处的某些 JVI 代码如何才能确保避免了常见缺陷或者在继承代码中发现它们表 1 提供了一些确定这些常见缺陷的技巧
表 1. 确定 JNI 编程缺陷的清单
未缓存触发数组副本错误界限过多回访使用大量本地引用使用错误的 JNIEnv未检测异常未检测返回值未正确使用数组未正确使用全局引用规范验证XXX方法跟踪XXXXXXX转储X-verbose:jniX代码审查XXXXXXXXXX
您可以在开发周期的早期确定许多常见缺陷方法如下
根据规范验证新代码分析方法跟踪使用-verbose:jni 选项生成转储执行代码审查
根据 JNI 规范验证新代码
维持规范的限制列表并审查本机与列表的遵从性是一个很好的实践这可以通过手动或自动代码分析来完成。确保遵从性的工作可能会比调试由于违背限制而出现的细小和间断性故障轻松很多。下面提供了一个专门针对新开发代码或对您来说是新的的规范顺从性检查列表
验证 JNIEnv 仅与与之相关的线程使用。确认未在 GetXXXCritical() 的 ReleaseXXXCritical() 部分调用 JNI 方法。对于进入关键部分的方法验证该方法未在释放前返回。验证在所有可能引起异常的 JNI 调用之前都检测了异常。确保所有 Get/Release 调用在各 JNI 方法中都是相匹配的。
IBM 的 JVM 实现包括开启自动 JNI 检测的选项其代价是较慢的执行速度。与出色的代码单元测试相结合这是一种极为强大的工具。您可以运行应用程序或单元测试来执行遵从性检查或者确定所遇到的 bug 是否是由本机引起的。除了执行上述规范遵从性检查之外它还能确保
传递给 JNI 方法的参数属于正确的类型。JNI 代码未读取超过数组结束部分之外的内容。传递给 JNI 方法的指针都是有效的。
JNI 检测报告的所有结论并不一定都是代码中的错误。它们还包括一些针对代码的建议您应该仔细阅读它们以确保代码功能正常。
您可以通过以下命令行启用 JNI 检测选项
Usage: -Xcheck:jni:[option[,option[,...]]]all check application and system classesverbose trace certain JNI functions and activitiestrace trace all JNI functionsnobounds do not perform bounds checking on strings and arraysnonfatal do not exit when errors are detectednowarn do not display warningsnoadvice do not display advicenovalist do not check for va_list reusevalist check for va_list reusepedantic perform more thorough, but slower checkshelp print this screen
使用 IBM JVM 的 -Xcheck:jni 选项作为标准开发流程的一部分可以帮助您更加轻松地找出代码错误。特别是它可以帮助您确定在错误线程中使用JNIEnv 以及未正确使用关键区域的缺陷的根源。
最新的 Sun JVM 提供了一个类似的 -Xcheck:jni 选项。它的工作原理不同于 IBM 版本并且提供了不同的信息但是它们的作用是相同的。它会在发现未符合规范的代码时发出警告并且可以帮助您确定常见的 JNI 缺陷。
分析方法跟踪
生成对已调用本机方法以及这些本机方法发起的 JNI 回调的跟踪这对确定大量常见缺陷的根源是非常有用的。可确定的问题包括
大量 GetFieldID() 和 GetMethodID() 调用 — 特别是如果这些调用针对相同的字段和方法 — 表示字段和方法未被缓存。GetTypeArrayElements() 调用实例而非 GetTypeArrayRegion()有时表示存在不必要的复制。在 Java 代码与本机代码之前来回快速切换由时间戳指示有时表示 Java 代码与本机代码之间的界限有误从而造成性能较差。每个本机函数调用后面都紧接着大量 GetFieldID() 调用这种模式表示并未传递所需的参数而是强制本机回访完成工作所需的数据。调用可能抛出异常的 JNI 方法之后缺少对 ExceptionOccurred() 或 ExceptionCheck() 的调用表示本机未正确检测异常。GetXXX() 和 ReleaseXXX() 方法调用的数量不匹配表示缺少释放操作。在 GetXXXCritical() 和 ReleaseXXXCritical() 调用之间调用 JNI 方法表示未遵循规范施加的限制。如果调用 GetXXXCritical() 和 ReleaseXXXCritical() 之间相隔的时间较长则表示未遵循 “不要阻塞调用” 规范所施加的限制。NewGlobalRef() 和 DeleteGlobalRef() 调用之间出现严重失衡表示释放不再需要的引用时出现故障。
一些 JVM 实现提供了一种可用于生存方法跟踪的机制。您还可以通过各种外部工具来生成跟踪比如探查器和代码覆盖工具。
IBM JVM 实现提供了许多用于生成跟踪信息的方法。第一种方法是使用 -Xcheck:jni:trace 选项。这将生成对已调用的本机方法以及它们发起的 JNI 回调的跟踪。代码 13 显示某个跟踪的摘录为便于阅读隔开了某些行
代码 13. IBM JVM 实现所生成的方法跟踪
Call JNI: java/lang/System.getPropertyList()[Ljava/lang/String; {
00177E00 Arguments: void
00177E00 FindClass(java/lang/String)
00177E00 FindClass(com/ibm/oti/util/Util)
00177E00 Call JNI: com/ibm/oti/vm/VM.useNativesImpl()Z {
00177E00 Arguments: void
00177E00 Return: (jboolean)false
00177E00 }
00177E00 Call JNI: java/security/AccessController.initializeInternal()V {
00177E00 Arguments: void
00177E00 FindClass(java/security/AccessController)
00177E00 GetStaticMethodID(java/security/AccessController, doPrivileged, (Ljava/security/PrivilegedAction;)Ljava/lang/Object;)
00177E00 GetStaticMethodID(java/security/AccessController, doPrivileged, (Ljava/security/PrivilegedExceptionAction;)Ljava/lang/Object;)
00177E00 GetStaticMethodID(java/security/AccessController, doPrivileged, (Ljava/security/PrivilegedAction;Ljava/security/AccessControlContext;)Ljava/lang/Object;)
00177E00 GetStaticMethodID(java/security/AccessController, doPrivileged, (Ljava/security/PrivilegedExceptionAction;Ljava/security/AccessControlContext;)Ljava/lang/Object;)
00177E00 Return: void
00177E00 }
00177E00 GetStaticMethodID(com/ibm/oti/util/Util, toString, ([BII)Ljava/lang/String;)
00177E00 NewByteArray((jsize)256)
00177E00 NewObjectArray((jsize)118, java/lang/String, (jobject)NULL)
00177E00 SetByteArrayRegion([B0018F7D0, (jsize)0, (jsize)30, (void*)7FF2E1D4)
00177E00 CallStaticObjectMethod/CallStaticObjectMethodV(com/ibm/oti/util/Util, toString([BII)Ljava/lang/String;, (va_list)0007D758) {
00177E00 Arguments: (jobject)0x0018F7D0, (jint)0, (jint)30
00177E00 Return: (jobject)0x0018F7C8
00177E00 }
00177E00 ExceptionCheck()
代码 13 中的跟踪摘录显示了已调用的本机方法比如 AccessController.initializeInternal()V以及本机方法发起的 JNI 回调。
使用 -verbose:jni 选项
Sun 和 IBM JVM 还提供了一个 -verbose:jni 选项。对于 IBM JVM 而言开启此选项将提供关于当前 JNI 回调的信息。代码 14 显示了一个示例
代码 14. 使用 IBM JVM 的 -verbose:jni 列出 JNI 回调
JNI GetStringCritical: buffer0x100BD010
JNI ReleaseStringCritical: buffer100BD010
JNI GetStringChars: buffer0x03019C88
JNI ReleaseStringChars: buffer03019C88
JNI FindClass: java/lang/String
JNI FindClass: java/io/WinNTFileSystem
JNI GetMethodID: java/io/WinNTFileSystem.init ()V
JNI GetStaticMethodID: com/ibm/j9/offload/tests/HelloWorld.main ([Ljava/lang/String;)V
JNI GetMethodID: java/lang/reflect/Method.getModifiers ()I
JNI FindClass: java/lang/String
对于 Sun JVM 而言开启 -verbose:jni 选项不会提供关于当前调用的信息但它会提供关于所使用的本机方法的额外信息。代码 15 显示了一个示例
代码 15. 使用 Sun JVM 的 -verbose:jni
[Dynamic-linking native method java.util.zip.ZipFile.getMethod ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.initIDs ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.init ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.inflateBytes ... JNI]
[Dynamic-linking native method java.util.zip.ZipFile.read ... JNI]
[Dynamic-linking native method java.lang.Package.getSystemPackage0 ... JNI]
[Dynamic-linking native method java.util.zip.Inflater.reset ... JNI]
开启此选项还会让 JVM 针对使用过多本地引用而未通知 JVM 的情况发起警告。举例来说IBM JVM 生成了这样一个消息
JVMJNCK065W JNI warning in FindClass: Automatically grew local reference frame capacity
from 16 to 48. 17 references are in use.
Use EnsureLocalCapacity or PushLocalFrame to explicitly grow the frame.
虽然 -verbose:jni 和 -Xcheck:jni:trace 选项可帮助您方便地获取所需的信息但手动审查此信息是一项艰巨的任务。一个不错的提议是创建一些脚本或实用工具来处理由 JVM 生成的跟踪文件并查看警告。 生成转储
运行中的 Java 进程生成的转储包含大量关于 JVM 状态的信息。对于许多 JVM 来说它们包括关于全局引用的信息。举例来说最新的 Sun JVM 在转储信息中包括这样一行
JNI global references: 73
通过生成前后转储您可以确定是否创建了任何未正常释放的全局引用。
您可以在 UNIX® 环境中通过对 java 进程发起 kill -3 或 kill -QUIT 来请求转储。在 Windows® 上使用 CtrlBreak 组合键。
对于 IBM JVM使用以下步骤获取关于全局引用的信息
将 -Xdump:system:eventsuser 添加到命令行。这样当您在 UNIX 系统上调用 kill -3 或者在 Windows 上按下 CtrlBreak 时JVM 便会生成转储。程序在运行中时会生成后续转储。运行 jextract -nozip core.XXX output.xml这将会将转储信息提取到可读格式的 output.xml 中。查找 output.xml 中的 JNIGlobalReference 条目它提供关于当前全局引用的信息如清单 16 所示
清单 16. output.xml 中的 JNIGlobalReference 条目
rootobject typeThread id0x10089990 reachabilitystrong /
rootobject typeThread id0x10089fd0 reachabilitystrong /
rootobject typeJNIGlobalReference id0x100100c0 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10011250 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10011840 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10011880 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10010af8 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10010360 reachabilitystrong /
rootobject typeJNIGlobalReference id0x10081f48 reachabilitystrong /
rootobject typeStringTable id0x10010be0 reachabilityweak /
rootobject typeStringTable id0x10010c70 reachabilityweak /
rootobject typeStringTable id0x10010d00 reachabilityweak /
rootobject typeStringTable id0x10011018 reachabilityweak /
通过查看后续 Java 转储中报告的数值您可以确定全局引用是否出现的泄漏。
参见 参考资料 获取关于使用转储文件以及 IBM JVM 的 jextract 的更多信息。
执行代码审查
代码审查经常可用于确定常见缺陷并且可以在各种级别上完成。继承新代码时快速扫描可以发现各种问题从而避免稍后花费更多时间进行调试。在某些情况下审查是确定缺陷实例比如未检查返回值的唯一方法。举例来说此代码的问题可能可以通过代码审查轻松确定但却很难通过调试来发现
int calledALot(JNIEnv* env, jobject obj, jobject allValues){jclass cls (*env)-GetObjectClass(env,allValues); jfieldID a (*env)-GetFieldID(env, cls, a, I);jfieldID b (*env)-GetFieldID(env, cls, b, I);jfieldID c (*env)-GetFieldID(env, cls, c, I);jfieldID d (*env)-GetFieldID(env, cls, d, I);jfieldID e (*env)-GetFieldID(env, cls, e, I);jfieldID f (*env)-GetFieldID(env, cls, f, I);}jclass getObjectClassHelper(jobject object){ /* use globally cached JNIEnv */return cls (*globalEnvStatic)-GetObjectClass(globalEnvStatic,allValues);
}
代码审查可能会发现第一个方法未正确缓存字段 ID尽管重复使用了相同的 ID并且第二个方法所使用的 JNIEnv 并不在应该在的线程上。
总结
本文阐述了 JNI 编程可能引发的内存泄漏JNI 编程既可能引发 Java Heap 的内存泄漏也可能引发 native memory 的内存泄漏严重的情况可能使 JVM 运行异常终止。JNI 软件开发人员在编程中应当考虑以下几点避免内存泄漏
native code 本身的内存管理机制依然要遵循。使用 Global reference 时当 native code 不再需要访问 Global reference 时应当调用 JNI 函数 DeleteGlobalRef() 删除 Global reference 和它引用的 Java 对象。Global reference 管理不当会导致 Java Heap 的内存泄漏。透彻理解 Local reference区分 Local reference 和 native code 的局部变量避免混淆两者所引起的 native memory 的内存泄漏。使用 Local reference 时如果 Local reference 引用了大的 Java 对象当不再需要访问 Local reference 时应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference从而也断开对 Java 对象的引用。这样可以避免 Java Heap 的 out of memory。使用 Local reference 时如果在 native method 执行期间会创建大量的 Local reference当不再需要访问 Local reference 时应当调用 JNI 函数 DeleteLocalRef() 删除 Local reference。Local reference 表空间有限这样可以避免 Local reference 表的内存溢出避免 native memory 的 out of memory。严格遵循 Java JNI 规范书中的使用规则。
文章转载自: http://www.morning.nrzkg.cn.gov.cn.nrzkg.cn http://www.morning.bqxxq.cn.gov.cn.bqxxq.cn http://www.morning.niukaji.com.gov.cn.niukaji.com http://www.morning.zdqsc.cn.gov.cn.zdqsc.cn http://www.morning.zbnts.cn.gov.cn.zbnts.cn http://www.morning.rqnzh.cn.gov.cn.rqnzh.cn http://www.morning.ljtwp.cn.gov.cn.ljtwp.cn http://www.morning.lzwfg.cn.gov.cn.lzwfg.cn http://www.morning.hwzzq.cn.gov.cn.hwzzq.cn http://www.morning.knlbg.cn.gov.cn.knlbg.cn http://www.morning.mngh.cn.gov.cn.mngh.cn http://www.morning.xplng.cn.gov.cn.xplng.cn http://www.morning.lfcnj.cn.gov.cn.lfcnj.cn http://www.morning.rzmkl.cn.gov.cn.rzmkl.cn http://www.morning.cwrpd.cn.gov.cn.cwrpd.cn http://www.morning.xhgxd.cn.gov.cn.xhgxd.cn http://www.morning.yuanshenglan.com.gov.cn.yuanshenglan.com http://www.morning.qwyms.cn.gov.cn.qwyms.cn http://www.morning.lwzgn.cn.gov.cn.lwzgn.cn http://www.morning.yrdn.cn.gov.cn.yrdn.cn http://www.morning.srnhk.cn.gov.cn.srnhk.cn http://www.morning.ckrnq.cn.gov.cn.ckrnq.cn http://www.morning.dmthy.cn.gov.cn.dmthy.cn http://www.morning.fmznd.cn.gov.cn.fmznd.cn http://www.morning.rgksz.cn.gov.cn.rgksz.cn http://www.morning.ktblf.cn.gov.cn.ktblf.cn http://www.morning.zhghd.cn.gov.cn.zhghd.cn http://www.morning.rhkmn.cn.gov.cn.rhkmn.cn http://www.morning.hrkth.cn.gov.cn.hrkth.cn http://www.morning.tfzjl.cn.gov.cn.tfzjl.cn http://www.morning.tjsxx.cn.gov.cn.tjsxx.cn http://www.morning.hpdpp.cn.gov.cn.hpdpp.cn http://www.morning.xxsrm.cn.gov.cn.xxsrm.cn http://www.morning.srzhm.cn.gov.cn.srzhm.cn http://www.morning.ngpdk.cn.gov.cn.ngpdk.cn http://www.morning.bpttm.cn.gov.cn.bpttm.cn http://www.morning.tfwr.cn.gov.cn.tfwr.cn http://www.morning.jtszm.cn.gov.cn.jtszm.cn http://www.morning.qhmgq.cn.gov.cn.qhmgq.cn http://www.morning.smtrp.cn.gov.cn.smtrp.cn http://www.morning.jxwhr.cn.gov.cn.jxwhr.cn http://www.morning.rxsgk.cn.gov.cn.rxsgk.cn http://www.morning.qdbcd.cn.gov.cn.qdbcd.cn http://www.morning.lbgfz.cn.gov.cn.lbgfz.cn http://www.morning.crqbt.cn.gov.cn.crqbt.cn http://www.morning.grcfn.cn.gov.cn.grcfn.cn http://www.morning.hbxnb.cn.gov.cn.hbxnb.cn http://www.morning.mmplj.cn.gov.cn.mmplj.cn http://www.morning.sfwd.cn.gov.cn.sfwd.cn http://www.morning.ffgbq.cn.gov.cn.ffgbq.cn http://www.morning.wsyq.cn.gov.cn.wsyq.cn http://www.morning.supera.com.cn.gov.cn.supera.com.cn http://www.morning.pmghz.cn.gov.cn.pmghz.cn http://www.morning.fcpjq.cn.gov.cn.fcpjq.cn http://www.morning.sqmlw.cn.gov.cn.sqmlw.cn http://www.morning.lmmkf.cn.gov.cn.lmmkf.cn http://www.morning.tbqdm.cn.gov.cn.tbqdm.cn http://www.morning.qfzjn.cn.gov.cn.qfzjn.cn http://www.morning.bhrkx.cn.gov.cn.bhrkx.cn http://www.morning.hjbrd.cn.gov.cn.hjbrd.cn http://www.morning.zfgh.cn.gov.cn.zfgh.cn http://www.morning.bxdlrcz.cn.gov.cn.bxdlrcz.cn http://www.morning.lsqmb.cn.gov.cn.lsqmb.cn http://www.morning.gxtbn.cn.gov.cn.gxtbn.cn http://www.morning.tclqf.cn.gov.cn.tclqf.cn http://www.morning.etsaf.com.gov.cn.etsaf.com http://www.morning.kqpxb.cn.gov.cn.kqpxb.cn http://www.morning.bccls.cn.gov.cn.bccls.cn http://www.morning.rgsgk.cn.gov.cn.rgsgk.cn http://www.morning.kllzy.com.gov.cn.kllzy.com http://www.morning.mprpx.cn.gov.cn.mprpx.cn http://www.morning.ljzss.cn.gov.cn.ljzss.cn http://www.morning.twwts.com.gov.cn.twwts.com http://www.morning.ffgbq.cn.gov.cn.ffgbq.cn http://www.morning.wfspn.cn.gov.cn.wfspn.cn http://www.morning.qhmhz.cn.gov.cn.qhmhz.cn http://www.morning.rdfq.cn.gov.cn.rdfq.cn http://www.morning.hrnrx.cn.gov.cn.hrnrx.cn http://www.morning.trhlb.cn.gov.cn.trhlb.cn http://www.morning.rjbb.cn.gov.cn.rjbb.cn