- 浏览: 96845 次
- 性别:
- 来自: 深圳
文章分类
最新评论
-
jkptthui:
入门级android程序员低调路过...
[转]Android高手应该精通哪些内容? -
andyzhchy:
makemelike 写道祝福一下你啦,想知道你现在过得怎么样 ...
辞职了,明年从头再来 -
makemelike:
祝福一下你啦,想知道你现在过得怎么样。
辞职了,明年从头再来 -
娴静女生:
这个在创建自定义的Adapter extends BaseAd ...
Android开发便签9:在android资源文件中定义字符串数组 -
nxxkve:
天朝的孩子纸伤不起
【转】同是80后 为什么程序员之间的差距那么大呢?
Java? 本机接口(Java Native Interface,JNI)是一个标准的 Java API,它支持将 Java 代码与使用其他编程语言编写的代码相集成。如果您希望利用已有的代码资源,那么可以使用 JNI 作为您工具包中的关键组件 —— 比如在面向服务架构(SOA)和基于云的系统中。但是,如果在使用时未注意某些事项,则 JNI 会迅速导致应用程序性能低下且不稳定。本文将确定 10 大 JNI 编程缺陷,提供避免这些缺陷的最佳实践,并介绍可用于实现这些实践的工具。
Java 环境和语言对于应用程序开发来说是非常安全和高效的。但是,一些应用程序却需要执行纯 Java 程序无法完成的一些任务,比如:
与旧有代码集成,避免重新编写。
实现可用类库中所缺少的功能。举例来说,在 Java 语言中实现 ping 时,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本类库并未提供它。
最好与使用 C/C++ 编写的代码集成,以充分发掘性能或其他与环境相关的系统特性。
解决需要非 Java 代码的特殊情况。举例来说,核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。
JNI 允许您完成这些任务。它明确分开了 Java 代码与本机代码(C/C++)的执行,定义了一个清晰的 API 在这两者之间进行通信。从很大程度上说,它避免了本机代码对 JVM 的直接内存引用,从而确保本机代码只需编写一次,并且可以跨不同的 JVM 实现或版本运行。
借助 JNI,本机代码可以随意与 Java 对象交互,获取和设计字段值,以及调用方法,而不会像 Java 代码中的相同功能那样受到诸多限制。这种自由是一把双刃剑:它牺牲 Java 代码的安全性,换取了完成上述所列任务的能力。在您的应用程序中使用 JNI 提供了强大的、对机器资源(内存、I/O 等)的低级访问,因此您不会像普通 Java 开发人员那样受到安全网的保护。JNI 的灵活性和强大性带来了一些编程实践上的风险,比如导致性能较差、出现 bug 甚至程序崩溃。您必须格外留意应用程序中的代码,并使用良好的实践来保障应用程序的总体完整性。
本文介绍 JNI 用户最常遇到的 10 大编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。
JNI 编程缺陷可以分为两类:
性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。
性能缺陷
程序员在使用 JNI 时的 5 大性能缺陷如下:
不缓存 方法 ID、字段 ID 和类
触发数组副 本
回 访(Reaching back)而不是传递参数
错 误认定本机代码与 Java 代码之间的界限
使用 大量本地引用,而未通知 JVM
不缓存方法 ID、字段 ID 和类
要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()、GetFieldID()、GetMethodId() 和 GetStaticMethodID()。对于 GetFieldID()、GetMethodID() 和 GetStaticMethodID(), 为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。
举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:
清单 1. 使用 JNI 调用静态方法
int val=1;
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;
}
性能技巧 #1
查找并全局缓存常用的类、字段 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 创建完整数组的副本。
性能技巧 #2
获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 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 倍的时间!
性能技巧 #3
在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。
另一方面,如果您最终要获取数组中的所有元素,则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 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;
}
性能技巧 #4
如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。
sumValues2() 方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues() 避免了大量的 JNI 开销。
错误认定本机代码与 Java 代码之间的界限
本 机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。
性能技巧 #5
定义 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 所示版本的一些问题会造成其性能差于清单 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);
性能技巧 #6
构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。
最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个 struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。
使用大量本地引用而未通知 JVM
JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用 GetObjectArrayElement() 时,将返回对数组中对象的本地引用。考虑清单 8 中的代码在运行一个很大的数组时会使用多少本地引用:
清单 8. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array){
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}
/* do something with array element */
}
}
每次调用 GetObjectArrayElement() 时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。
性能技巧 #7
当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。
这些本地引用会在本机方法终止时自动释放。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(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}
/* do something with array element */
(*env)->DeleteLocalRef(env, element);
}
}
性能技巧 #8
如果某本机代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。
您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError。
回页首
正确性缺陷
5 大 JNI 正确性缺陷包括:
使用 错误的 JNIEnv
未检测异常
未 检测返回值
未正确使 用数组方法
未 正确使用全局引用
使用错误的 JNIEnv
执行本机代码的线程使用 JNIEnv 发起 JNI 方法调用。但是,JNIEnv 并不是仅仅用于分派所请求的方法。JNI 规范规定每个 JNIEnv 对于线程来说都是本地的。JVM 可以依赖于这一假设,将额外的线程本地信息存储在 JNIEnv 中。一个线程使用另一个线程中的 JNIEnv 会导致一些小 bug 和难以调试的崩溃问题。
正确性技巧 #1
仅在相关的单一线程中使用 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. 未能检测异常
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);
正确性技巧 #2
在发起可能会导致异常的 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 方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。
正确性技巧 #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() 不存在,则本机将造成程序崩溃。
未正确使用数组方法
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 调用之间执行的所有代码,无论它是不是本机的一部分。
未正确使用全局引用
本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
jobject gref = (*env)->NewGlobalRef(env, keepObj);
}
创 建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。
避免常见缺陷
假设您编写了一些新 JNI 代码,或者继承了别处的某些 JVI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?表 1 提供了一些确定这些常见缺陷的技巧:
表 1. 确定 JNI 编程缺陷的清单
未缓存 触发数组副本 错误界限 过多回访 使用大量本地引用 使用错误的 JNIEnv 未检测异常 未 检测返回值 未正确使用数组 未正确使用全局引用
规范验证
X X
X
方法跟踪 X X X X
X
X X
转储
X
-verbose:jni
X
代码审查 X X X X X X X X X X
您可以在开发周期的早期确定许多常见缺陷,方法如下:
根据规范验 证新代码
分析 方法跟踪
使 用 -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 classes
verbose trace certain JNI functions and activities
trace trace all JNI functions
nobounds do not perform bounds checking on strings and arrays
nonfatal do not exit when errors are detected
nowarn do not display warnings
noadvice do not display advice
novalist do not check for va_list reuse
valist check for va_list reuse
pedantic perform more thorough, but slower checks
help 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([B@0018F7D0, (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: buffer=0x100BD010>
<JNI ReleaseStringCritical: buffer=100BD010>
<JNI GetStringChars: buffer=0x03019C88>
<JNI ReleaseStringChars: buffer=03019C88>
<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? 上,使用 Ctrl+Break 组合键。
对于 IBM JVM,使用以下步骤获取关于全局引用的信息:
将 -Xdump:system:events=user 添加到命令行。这样,当您在 UNIX 系统上调用 kill -3 或者在 Windows 上按下 Ctrl+Break 时,JVM 便会生成转储。
程序在运行中时会生成后续转储。
运行 jextract -nozip core.XXX output.xml,这将会将转储信 息提取到可读格式的 output.xml 中。
查找 output.xml 中的 JNIGlobalReference 条目,它提供关于当前全局引用的信息,如清单 16 所示:
清单 16. output.xml 中的 JNIGlobalReference 条目
<rootobject type="Thread" id="0x10089990" reachability="strong" />
<rootobject type="Thread" id="0x10089fd0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" />
<rootobject type="StringTable" id="0x10010be0" reachability="weak" />
<rootobject type="StringTable" id="0x10010c70" reachability="weak" />
<rootobject type="StringTable" id="0x10010d00" reachability="weak" />
<rootobject type="StringTable" id="0x10011018" reachability="weak" />
通过查看后续 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 并不在应该在的线程上。
结束语
现在,您已经了解了 10 大 JNI 编程缺陷,以及一些用于在已有或新代码中确定它们的良好实践。坚持应用这些实践有助于提高 JNI 代码的正确率,并且您的应用程序可以实现所需的性能水平。
有 效集成已有代码资源的能力对于面向对象架构(SOA)和基于云的计算这两种技术的成功至关重要。JNI 是一项非常重要的技术,用于将非 Java 旧有代码和组件集成到基于 Java 的平台中,充当 SOA 或基于云的系统的基本元素。正确使用 JNI 可以加速将这些组件转变为服务的过程,并允许您从现有投资中获得最大优势。
注:本文转自http://www.360doc.com/content/10/0910/15/2705099_52644635.shtml
Java 环境和语言对于应用程序开发来说是非常安全和高效的。但是,一些应用程序却需要执行纯 Java 程序无法完成的一些任务,比如:
与旧有代码集成,避免重新编写。
实现可用类库中所缺少的功能。举例来说,在 Java 语言中实现 ping 时,您可能需要 Internet Control Message Protocol (ICMP) 功能,但基本类库并未提供它。
最好与使用 C/C++ 编写的代码集成,以充分发掘性能或其他与环境相关的系统特性。
解决需要非 Java 代码的特殊情况。举例来说,核心类库的实现可能需要跨包调用或者需要绕过其他 Java 安全性检查。
JNI 允许您完成这些任务。它明确分开了 Java 代码与本机代码(C/C++)的执行,定义了一个清晰的 API 在这两者之间进行通信。从很大程度上说,它避免了本机代码对 JVM 的直接内存引用,从而确保本机代码只需编写一次,并且可以跨不同的 JVM 实现或版本运行。
借助 JNI,本机代码可以随意与 Java 对象交互,获取和设计字段值,以及调用方法,而不会像 Java 代码中的相同功能那样受到诸多限制。这种自由是一把双刃剑:它牺牲 Java 代码的安全性,换取了完成上述所列任务的能力。在您的应用程序中使用 JNI 提供了强大的、对机器资源(内存、I/O 等)的低级访问,因此您不会像普通 Java 开发人员那样受到安全网的保护。JNI 的灵活性和强大性带来了一些编程实践上的风险,比如导致性能较差、出现 bug 甚至程序崩溃。您必须格外留意应用程序中的代码,并使用良好的实践来保障应用程序的总体完整性。
本文介绍 JNI 用户最常遇到的 10 大编码和设计错误。其目标是帮助您认识到并避免它们,以便您可以编写安全、高效、性能出众的 JNI 代码。本文还将介绍一些用于在新代码或已有代码中查找这些问题的工具和技巧,并展示如何有效地应用它们。
JNI 编程缺陷可以分为两类:
性能:代码能执行所设计的功能,但运行缓慢或者以某种形式拖慢整个程序。
正确性:代码有时能正常运行,但不能可靠地提供所需的功能;最坏的情况是造成程序崩溃或挂起。
性能缺陷
程序员在使用 JNI 时的 5 大性能缺陷如下:
不缓存 方法 ID、字段 ID 和类
触发数组副 本
回 访(Reaching back)而不是传递参数
错 误认定本机代码与 Java 代码之间的界限
使用 大量本地引用,而未通知 JVM
不缓存方法 ID、字段 ID 和类
要访问 Java 对象的字段并调用它们的方法,本机代码必须调用 FindClass()、GetFieldID()、GetMethodId() 和 GetStaticMethodID()。对于 GetFieldID()、GetMethodID() 和 GetStaticMethodID(), 为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。
举例来说,清单 1 展示了调用静态方法所需的 JNI 代码:
清单 1. 使用 JNI 调用静态方法
int val=1;
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;
}
性能技巧 #1
查找并全局缓存常用的类、字段 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 创建完整数组的副本。
性能技巧 #2
获取和更新仅本机代码需要的数组部分。在只要数组的一部分时通过适当的 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 倍的时间!
性能技巧 #3
在单个 API 调用中尽可能多地获取或更新数组内容。如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。
另一方面,如果您最终要获取数组中的所有元素,则使用 GetTypeArrayRegion() 逐个获取数组中的元素是得不偿失的。要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。如果您要迭代一个数组中的所有元素,则 清单 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;
}
性能技巧 #4
如果可能,将各参数传递给 JNI 本机代码,以便本机代码回调 JVM 获取所需的数据。
sumValues2() 方法需要 6 个 JNI 回调,并且运行 10,000,000 次需要 3,572 ms。其速度比 sumValues() 慢 6 倍,后者只需要 596 ms。通过传递 JNI 方法所需的数据,sumValues() 避免了大量的 JNI 开销。
错误认定本机代码与 Java 代码之间的界限
本 机代码和 Java 代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从 Java 代码中调用本机代码以及从本机代码调用 Java 代码的开销比普通的 Java 方法调用高很多。此外,这种越界操作会干扰 JVM 优化代码执行的能力。举例来说,随着 Java 代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从 Java 代码调用本机代码要比普通调用多花 5 倍的时间。同样,从本机代码中调用 Java 代码也需要耗费大量的时间。
性能技巧 #5
定义 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 所示版本的一些问题会造成其性能差于清单 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);
性能技巧 #6
构造应用程序的数据,使它位于界限的正确的侧,并且可以由使用它的代码访问,而不需要大量跨界调用。
最显著的一个问题就是,清单 6 中的接口在设置或检索每个位,以及从串行端口读取字节或者向串行端口写入字节都需要一个 JNI 调用。这会导致读取或写入的每个字节的 JNI 调用变成原来的 9 倍。第二个问题是,清单 6 将串行端口的配置信息存储在 Java/本机界限的错误一侧的某个 Java 对象上。我们仅在本机侧需要此配置数据;将它存储在 Java 侧会导致本机代码向 Java 代码发起大量回调以获取/设置此配置信息。清单 7 将配置信息存储在一个本机结构中(比如,一个 struct),并向 Java 代码返回了一个不透明的句柄,该句柄可以在后续调用中返回。这意味着,当本机代码正在运行时,它可以直接访问该结构,而不需要回调 Java 代码获取串行端口硬件地址或下一个可用的缓冲区等信息。因此,使用 清单 7 的实现的性能将大大改善。
使用大量本地引用而未通知 JVM
JNI 函数返回的任何对象都会创建本地引用。举例来说,当您调用 GetObjectArrayElement() 时,将返回对数组中对象的本地引用。考虑清单 8 中的代码在运行一个很大的数组时会使用多少本地引用:
清单 8. 创建本地引用
void workOnArray(JNIEnv* env, jobject obj, jarray array){
jint i;
jint count = (*env)->GetArrayLength(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}
/* do something with array element */
}
}
每次调用 GetObjectArrayElement() 时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。
性能技巧 #7
当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。
这些本地引用会在本机方法终止时自动释放。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(env, array);
for (i=0; i < count; i++) {
jobject element = (*env)->GetObjectArrayElement(env, array, i);
if((*env)->ExceptionOccurred(env)) {
break;
}
/* do something with array element */
(*env)->DeleteLocalRef(env, element);
}
}
性能技巧 #8
如果某本机代码将同时存在大量本地引用,则调用 JNI EnsureLocalCapacity() 方法通知 JVM 并允许它优化对本地引用的处理。
您可以调用 JNI EnsureLocalCapacity() 方法来通知 JVM 您将使用超过 16 个本地引用。这将允许 JVM 优化对该本机代码的本地引用的处理。如果无法创建所需的本地引用,或者 JVM 采用的本地引用管理方法与所使用的本地引用数量之间不匹配造成了性能低下,则未成功通知 JVM 会导致 FatalError。
回页首
正确性缺陷
5 大 JNI 正确性缺陷包括:
使用 错误的 JNIEnv
未检测异常
未 检测返回值
未正确使 用数组方法
未 正确使用全局引用
使用错误的 JNIEnv
执行本机代码的线程使用 JNIEnv 发起 JNI 方法调用。但是,JNIEnv 并不是仅仅用于分派所请求的方法。JNI 规范规定每个 JNIEnv 对于线程来说都是本地的。JVM 可以依赖于这一假设,将额外的线程本地信息存储在 JNIEnv 中。一个线程使用另一个线程中的 JNIEnv 会导致一些小 bug 和难以调试的崩溃问题。
正确性技巧 #1
仅在相关的单一线程中使用 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. 未能检测异常
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);
正确性技巧 #2
在发起可能会导致异常的 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 方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。
正确性技巧 #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() 不存在,则本机将造成程序崩溃。
未正确使用数组方法
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 调用之间执行的所有代码,无论它是不是本机的一部分。
未正确使用全局引用
本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:
lostGlobalRef(JNIEnv* env, jobject obj, jobject keepObj) {
jobject gref = (*env)->NewGlobalRef(env, keepObj);
}
创 建全局引用时,JVM 会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它 — 因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。
避免常见缺陷
假设您编写了一些新 JNI 代码,或者继承了别处的某些 JVI 代码,如何才能确保避免了常见缺陷,或者在继承代码中发现它们?表 1 提供了一些确定这些常见缺陷的技巧:
表 1. 确定 JNI 编程缺陷的清单
未缓存 触发数组副本 错误界限 过多回访 使用大量本地引用 使用错误的 JNIEnv 未检测异常 未 检测返回值 未正确使用数组 未正确使用全局引用
规范验证
X X
X
方法跟踪 X X X X
X
X X
转储
X
-verbose:jni
X
代码审查 X X X X X X X X X X
您可以在开发周期的早期确定许多常见缺陷,方法如下:
根据规范验 证新代码
分析 方法跟踪
使 用 -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 classes
verbose trace certain JNI functions and activities
trace trace all JNI functions
nobounds do not perform bounds checking on strings and arrays
nonfatal do not exit when errors are detected
nowarn do not display warnings
noadvice do not display advice
novalist do not check for va_list reuse
valist check for va_list reuse
pedantic perform more thorough, but slower checks
help 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([B@0018F7D0, (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: buffer=0x100BD010>
<JNI ReleaseStringCritical: buffer=100BD010>
<JNI GetStringChars: buffer=0x03019C88>
<JNI ReleaseStringChars: buffer=03019C88>
<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? 上,使用 Ctrl+Break 组合键。
对于 IBM JVM,使用以下步骤获取关于全局引用的信息:
将 -Xdump:system:events=user 添加到命令行。这样,当您在 UNIX 系统上调用 kill -3 或者在 Windows 上按下 Ctrl+Break 时,JVM 便会生成转储。
程序在运行中时会生成后续转储。
运行 jextract -nozip core.XXX output.xml,这将会将转储信 息提取到可读格式的 output.xml 中。
查找 output.xml 中的 JNIGlobalReference 条目,它提供关于当前全局引用的信息,如清单 16 所示:
清单 16. output.xml 中的 JNIGlobalReference 条目
<rootobject type="Thread" id="0x10089990" reachability="strong" />
<rootobject type="Thread" id="0x10089fd0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x100100c0" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011250" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011840" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10011880" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010af8" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10010360" reachability="strong" />
<rootobject type="JNIGlobalReference" id="0x10081f48" reachability="strong" />
<rootobject type="StringTable" id="0x10010be0" reachability="weak" />
<rootobject type="StringTable" id="0x10010c70" reachability="weak" />
<rootobject type="StringTable" id="0x10010d00" reachability="weak" />
<rootobject type="StringTable" id="0x10011018" reachability="weak" />
通过查看后续 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 并不在应该在的线程上。
结束语
现在,您已经了解了 10 大 JNI 编程缺陷,以及一些用于在已有或新代码中确定它们的良好实践。坚持应用这些实践有助于提高 JNI 代码的正确率,并且您的应用程序可以实现所需的性能水平。
有 效集成已有代码资源的能力对于面向对象架构(SOA)和基于云的计算这两种技术的成功至关重要。JNI 是一项非常重要的技术,用于将非 Java 旧有代码和组件集成到基于 Java 的平台中,充当 SOA 或基于云的系统的基本元素。正确使用 JNI 可以加速将这些组件转变为服务的过程,并允许您从现有投资中获得最大优势。
注:本文转自http://www.360doc.com/content/10/0910/15/2705099_52644635.shtml
发表评论
-
ListView 最后一行之后没有出现分割线
2012-08-09 10:57 3962其他界面的ListView都可以,也检查了代码,都一样啊,就差 ... -
利用HTML5开发Android
2012-03-14 10:17 2271● Android设备多分辨率的问题 Android浏览器默认 ... -
android 听筒播放音乐
2012-03-05 17:12 1791最近遇到一个需求需要用听筒播放声音,然后就搜了一下,找到下面这 ... -
[转]Show Stopper 一次 crash 调试的夺命狂奔
2012-01-10 10:48 2352这几天一直在忙着调试 crash 的问题。周末两天都在加班。 ... -
在 JNI 编程中避免内存泄漏
2011-12-26 10:16 4125JNI 编程简介 JNI,Java Nat ... -
关于ReferenceTable overflow (max=512)的解决
2011-12-01 11:07 1326关于ReferenceTable overflow (max= ... -
android的jni.h头文件
2011-12-01 10:55 1057上传一个jni.h的头文件 -
Android,去除Dialog的边框
2011-10-19 14:49 3985a.首先为这个半透明弹窗新建一个名为dialog.xml的La ... -
Android 绘制带有边框的文字
2011-10-19 14:46 2255/*** * 绘制带有边框的文字 * * @par ... -
Android开发便签9:在android资源文件中定义字符串数组
2011-06-16 10:35 3563小便签一个,如何在android的资源文件中定义字符串数组及在 ... -
【转】创建和使用Android library工程
2011-05-11 11:08 985创建library供多个工程共享代码、资源是非常常见的需求,网 ... -
[转]Android高手应该精通哪些内容?
2011-03-28 09:59 1099很多Android开发者已经度过了初级、中级,如何成为一个An ... -
Android开发便签8:程序退出时将自己关闭的方法
2011-03-12 12:14 1029有时候我们希望在我们点击返回键退出这个程序时关闭这个应用,可是 ... -
Android开发便签7:如何让通讯录匹配N位号码
2010-11-04 19:33 1947Android的系统自带的是默认匹配7位的号码,即如果你有一个 ... -
Android应用协调器------Intent
2010-10-21 15:20 778刚起步的时候经常困扰我们的是一些本来容易解决的问题,往往我们会 ... -
Android游戏引擎选择
2010-10-18 11:02 1251今天在博客园看到一篇关于android游戏引擎的选择文章,特转 ... -
Android源码获取与编译
2010-09-23 08:58 1460如何编译Android的源码,一下是从网络上查找的资料,经过自 ... -
享受Android应用程序的Java技术盛宴
2010-09-12 01:31 1011准备开始 本文介绍几 ... -
Android开发便签6:数据保存之File存储
2010-09-01 15:14 1534Android的数据存储有一下几种方式: 1、Shared P ... -
Android开发便签5:如何将一个Activity设置成全屏显示
2010-08-06 17:00 1712有时候我们希望我们界面是全屏显示,该怎么做呢? 其实很简单,在 ...
相关推荐
### JNI最佳实践 #### 性能缺陷 JNI作为一种强大的技术,允许C/C++代码与Java代码无缝交互,但在实际开发过程中如果不注意某些细节,则可能导致性能下降。以下将详细介绍常见的五种性能缺陷及其应对策略。 1. **...
本示例“jni最佳实践demo”涵盖了多个关键的JNI使用场景,包括数据类型转换、方法调用以及内存管理。 1. 传String: 在JNI中处理Java的字符串通常涉及到`jstring`类型的使用。首先,Java端创建字符串并传递给JNI...
- **最佳实践**:关于编写高效、安全和可维护的JNI代码的建议。 - **案例研究**:实际应用中的JNI实现,例如与图形库、音频处理库或其他系统服务的集成。 4. **Java_JNI编程进阶** 进阶内容可能包括: - **动态...
在Android开发中,JNI(Java Native Interface)是一个关键的组件,它允许Java代码与其他语言写的代码进行交互。...因此,应谨慎选择何时使用JNI,并确保遵循最佳实践,以确保应用的稳定性和性能。
12. **最佳实践**:在使用JNI时,遵循一些最佳实践,如避免过度使用JNI,尽可能保持Java代码的纯度;尽量减少Java和本地代码之间的数据交换,因为数据拷贝会增加开销。 综上所述,"JNI程序员指南与规范"涵盖了JNI的...
提供的文档如"JNI-NDK 开发指南 - v1.0"和"JNI编程指南"是学习JNI的重要资料,它们详细解释了JNI的使用方法和最佳实践。英文原版和中文版的"the java Native Interface"可以帮助开发者深入理解JNI的内部工作原理。 ...
尽管JNI提供了强大的功能,但在使用时需遵循一定的最佳实践,以确保项目的稳定性和可维护性: - **集中本地方法**:将本地方法的实现集中在少数几个类中,减少Java与C/C++之间的耦合度,便于管理和调试。 - **评估...
7. **最佳实践**:可能包含了一些使用JNI时的最佳实践,如避免内存泄漏,正确管理资源等。 在压缩包中的"src"目录下,很可能包含了Java源代码文件(.java)、JNI的头文件(.h)以及本地实现的C/C++源代码(.c或.cpp...
10. **最佳实践**:使用JNI时,建议保持本地方法尽可能简单,避免大量业务逻辑在本地代码中,以降低维护难度。同时,对错误处理进行充分测试,确保在各种情况下程序的稳定性。 通过阅读"JNI完全技术手册",开发者...
书中可能包含一些最佳实践,如减少本地方法的调用次数,缓存`JNIEnv`指针,以及合理使用局部引用。 9. **案例分析**:为了巩固理论知识,书中可能提供了一些实际应用示例,比如使用JNI实现图形加速、音视频解码,...
11. JNI的使用场景和最佳实践:JNI虽然功能强大,但使用不当可能会导致代码难以维护和容易出错,因此了解何时以及如何正确使用JNI非常重要。 12. JNI相关的工具和调试:JNI编程可能涉及到复杂的调试工作,因此了解...
chap 17:使用 Java Native Interface 的最佳实践... 106 1.性能缺陷... 107 2.正确性缺陷... 117 3.避免常见缺陷... 121 4.结束语... 128 Chap18:JNI设计实践之路... 129 一、 前言... 129 二、 JNI基础知识...
10. **最佳实践**:尽量将复杂的计算和低级操作放在本地代码中,保持Java代码简洁。同时,确保本地代码的错误处理充分,避免程序崩溃。 总之,"android studio JNI demo"项目是一个学习和实践JNI技术的好起点,通过...
2. **JNI_编程技术__网文整理(1).doc 和 JNI_编程技术__网文整理.doc**:这些文档可能包含从网络上收集的各种JNI编程技巧和最佳实践,包括但不限于JNI函数的声明、数据类型的映射、异常处理、多线程支持等内容。...
此外,为了保证代码的安全性和效率,需要对敏感数据进行妥善处理,遵循最佳实践,例如避免内存泄漏和确保数据在传输过程中的安全性。 总之,这个压缩包提供了一种在Java环境中利用GMSSL国密算法的解决方案,通过JNI...
- 加密过程应遵循安全编码最佳实践,防止中间人攻击和其他安全漏洞。 - 了解并遵循相关的法律法规,尤其是在处理用户数据时。 8. **性能优化**:虽然原生代码通常比Java更快,但过度依赖JNI可能导致内存管理复杂...
5. **注意事项和最佳实践** - 确保Java和C++的数据类型兼容,例如,Java的`int`对应C++的`jint`。 - 错误处理:在C++本地方法中,使用JNI的错误处理机制来捕获和报告异常。 - 平台兼容性:确保生成的库是针对目标...
这本书可能深入介绍了JNI的最新版本和最佳实践,包括: - 更新的JNI函数和特性 - 对64位系统和多版本Java环境的支持 - 更高级的用法,如JNI与Java反射的结合 - 实战案例和示例代码 总结,JNI是连接Java世界与本地...
随着Android系统的不断演进,JNI的使用也有了新的规范和最佳实践。例如,旧版本的JNI可能需要开发者手动管理内存,而新版本则推荐使用智能指针(如C++11的`std::unique_ptr`或`std::shared_ptr`)来自动管理。此外...
在Android开发中,有时我们需要利用高性能的C++代码来实现某些功能,比如加密。AES(Advanced Encryption Standard)...记得在实际项目中,还需要考虑错误处理、资源管理以及安全最佳实践,如密钥安全存储和内存清理。