使用 Java Native Interface 的最佳实践.docx
- 文档编号:25465569
- 上传时间:2023-06-09
- 格式:DOCX
- 页数:40
- 大小:32.44KB
使用 Java Native Interface 的最佳实践.docx
《使用 Java Native Interface 的最佳实践.docx》由会员分享,可在线阅读,更多相关《使用 Java Native Interface 的最佳实践.docx(40页珍藏版)》请在冰豆网上搜索。
使用JavaNativeInterface的最佳实践
使用JavaNativeInterface的最佳实践
简介:
Java™本机接口(JavaNativeInterface,JNI)是一个标准的JavaAPI,它支持将Java代码与使用其他编程语言编写的代码相集成。
如果您希望利用已有的代码资源,那么可以使用JNI作为您工具包中的关键组件——比如在面向服务架构(SOA)和基于云的系统中。
但是,如果在使用时未注意某些事项,则JNI会迅速导致应用程序性能低下且不稳定。
本文将确定10大JNI编程缺陷,提供避免这些缺陷的最佳实践,并介绍可用于实现这些实践的工具。
Java环境和语言对于应用程序开发来说是非常安全和高效的。
但是,一些应用程序却需要执行纯Java程序无法完成的一些任务,比如:
JNI的发展
JNI自从JDK1.1发行版以来一直是Java平台的一部分,并且在JDK1.2发行版中得到了扩展。
JDK1.0发行版包含一个早期的本机方法接口,但是未明确分隔本机代码和Java代码。
在这个接口中,本机代码可以直接进入JVM结构,因此无法跨JVM实现、平台或者甚至各种JDK版本进行移植。
使用JDK1.0模型升级含有大量本机代码的应用程序,以及开发能支持多个JVM实现的本机代码的开销是极高的。
JDK1.1中引入的JNI支持:
●版本独立性
●平台独立性
●VM独立性
●开发第三方类库
有一个有趣的地方值得注意,一些较年轻的语言(如PHP)在它们的本机代码支持方面仍然在努力克服这些问题。
●与旧有代码集成,避免重新编写。
●实现可用类库中所缺少的功能。
举例来说,在Java语言中实现ping时,您可能需要InternetControlMessageProtocol(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和类
●触发数组副本
●回访(Reachingback)而不是传递参数
●错误认定本机代码与Java代码之间的界限
●使用大量本地引用,而未通知JVM
不缓存方法ID、字段ID和类
要访问Java对象的字段并调用它们的方法,本机代码必须调用FindClass()、GetFieldID()、GetMethodId()和GetStaticMethodID()。
对于GetFieldID()、GetMethodID()和GetStaticMethodID(),为特定类返回的ID不会在JVM进程的生存期内发生变化。
但是,获取字段或方法的调用有时会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让JVM向上遍历类层次结构来找到它们。
由于ID对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。
同样,查找类对象的开销也很大,因此也应该缓存它们。
举例来说,清单1展示了调用静态方法所需的JNI代码:
清单1.使用JNI调用静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16intval=1;
jmethodIDmethod;
jclasscls;
cls=(*env)->FindClass(env,"com/ibm/example/TestClass");
if((*env)->ExceptionCheck(env)){
returnERR_FIND_CLASS_FAILED;
}
method=(*env)->GetStaticMethodID(env,cls,"setInfo","(I)V");
if((*env)->ExceptionCheck(env)){
returnERR_GET_STATIC_METHOD_FAILED;
}
(*env)->CallStaticVoidMethod(env,cls,method,val);
if((*env)->ExceptionCheck(env)){
returnERR_CALL_STATIC_METHOD_FAILED;
}
当我们每次希望调用方法时查找类和方法ID都会产生六个本机调用,而不是第一次缓存类和方法ID时需要的两个调用。
缓存会对您应用程序的运行时造成显著的影响。
考虑下面两个版本的方法,它们的作用是相同的。
清单2使用了缓存的字段ID:
清单2.使用缓存的字段ID
1
2
3
4
5
6
7
8
9
10
11intsumValues2(JNIEnv*env,jobjectobj,jobjectallValues){
jintavalue=(*env)->GetIntField(env,allValues,a);
jintbvalue=(*env)->GetIntField(env,allValues,b);
jintcvalue=(*env)->GetIntField(env,allValues,c);
jintdvalue=(*env)->GetIntField(env,allValues,d);
jintevalue=(*env)->GetIntField(env,allValues,e);
jintfvalue=(*env)->GetIntField(env,allValues,f);
returnavalue+bvalue+cvalue+dvalue+evalue+fvalue;
}
性能技巧#1
查找并全局缓存常用的类、字段ID和方法ID。
清单3没有使用缓存的字段ID:
清单3.未缓存字段ID
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16intsumValues2(JNIEnv*env,jobjectobj,jobjectallValues){
jclasscls=(*env)->GetObjectClass(env,allValues);
jfieldIDa=(*env)->GetFieldID(env,cls,"a","I");
jfieldIDb=(*env)->GetFieldID(env,cls,"b","I");
jfieldIDc=(*env)->GetFieldID(env,cls,"c","I");
jfieldIDd=(*env)->GetFieldID(env,cls,"d","I");
jfieldIDe=(*env)->GetFieldID(env,cls,"e","I");
jfieldIDf=(*env)->GetFieldID(env,cls,"f","I");
jintavalue=(*env)->GetIntField(env,allValues,a);
jintbvalue=(*env)->GetIntField(env,allValues,b);
jintcvalue=(*env)->GetIntField(env,allValues,c);
jintdvalue=(*env)->GetIntField(env,allValues,d);
jintevalue=(*env)->GetIntField(env,allValues,e);
jintfvalue=(*env)->GetIntField(env,allValues,f);
returnavalue+bvalue+cvalue+dvalue+evalue+fvalue
}
清单2用3,572ms运行了10,000,000次。
清单3用了86,217ms—多花了24倍的时间。
触发数组副本
JNI在Java代码和本机代码之间提供了一个干净的接口。
为了维持这种分离,数组将作为不透明的句柄传递,并且本机代码必须回调JVM以便使用set和get调用操作数组元素。
Java规范让JVM实现决定让这些调用提供对数组的直接访问,还是返回一个数组副本。
举例来说,当数组经过优化而不需要连续存储时,JVM可以返回一个副本。
(参见参考资料获取关于JVM的信息)。
随后,这些调用可以复制被操作的元素。
举例来说,如果您对含有1,000个元素的数组调用GetLongArrayElements(),则会造成至少分配或复制8,000字节的数据(每个long1,000元素*8字节)。
当您随后使用ReleaseLongArrayElements()更新数组的内容时,需要另外复制8,000字节的数据来更新数组。
即使您使用较新的GetPrimitiveArrayCritical(),规范仍然准许JVM创建完整数组的副本。
性能技巧#2
获取和更新仅本机代码需要的数组部分。
在只要数组的一部分时通过适当的API调用来避免复制整个数组。
GetTypeArrayRegion()和SetTypeArrayRegion()方法允许您获取和更新数组的一部分,而不是整个数组。
通过使用这些方法访问较大的数组,您可以确保只复制本机代码将要实际使用的数组部分。
举例来说,考虑相同方法的两个版本,如清单4所示:
清单4.相同方法的两个版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16jlonggetElement(JNIEnv*env,jobjectobj,jlongArrayarr_j,
intelement){
jbooleanisCopy;
jlongresult;
jlong*buffer_j=(*env)->GetLongArrayElements(env,arr_j,&isCopy);
result=buffer_j[element];
(*env)->ReleaseLongArrayElements(env,arr_j,buffer_j,0);
returnresult;
}
jlonggetElement2(JNIEnv*env,jobjectobj,jlongArrayarr_j,
intelement){
jlongresult;
(*env)->GetLongArrayRegion(env,arr_j,element,1,&result);
returnresult;
}
第一个版本可以生成两个完整的数组副本,而第二个版本则完全没有复制数组。
当数组大小为1,000字节时,运行第一个方法10,000,000次用了12,055ms;而第二个版本仅用了1,421ms。
第一个版本多花了8.5倍的时间!
性能技巧#3
在单个API调用中尽可能多地获取或更新数组内容。
如果可以一次较多地获取和更新数组内容,则不要逐个迭代数组中的元素。
另一方面,如果您最终要获取数组中的所有元素,则使用GetTypeArrayRegion()逐个获取数组中的元素是得不偿失的。
要获取最佳的性能,应该确保以尽可能大的块的来获取和更新数组元素。
如果您要迭代一个数组中的所有元素,则清单4中这两个getElement()方法都不适用。
比较好的方法是在一个调用中获取大小合理的数组部分,然后再迭代所有这些元素,重复操作直到覆盖整个数组。
回访而不是传递参数
在调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。
在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变化不需要改变方法签名。
但是,对于JNI来说,本机代码必须通过一个或多个JNI调用返回到JVM以获取需要的各个字段的值。
这些额外的调用会带来额外的开销,因为从本机代码过渡到Java代码要比普通方法调用开销更大。
因此,对于JNI来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。
考虑清单5中的两个方法,第二个方法假定我们缓存了字段ID:
清单5.两个方法版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15intsumValues(JNIEnv*env,jobjectobj,jinta,jintb,jintc,jintd,jinte,jintf){
returna+b+c+d+e+f;
}
intsumValues2(JNIEnv*env,jobjectobj,jobjectallValues){
jintavalue=(*env)->GetIntField(env,allValues,a);
jintbvalue=(*env)->GetIntField(env,allValues,b);
jintcvalue=(*env)->GetIntField(env,allValues,c);
jintdvalue=(*env)->GetIntField(env,allValues,d);
jintevalue=(*env)->GetIntField(env,allValues,e);
jintfvalue=(*env)->GetIntField(env,allValues,f);
returnavalue+bvalue+cvalue+dvalue+evalue+fvalue;
}
性能技巧#4
如果可能,将各参数传递给JNI本机代码,以便本机代码回调JVM获取所需的数据。
sumValues2()方法需要6个JNI回调,并且运行10,000,000次需要3,572ms。
其速度比sumValues()慢6倍,后者只需要596ms。
通过传递JNI方法所需的数据,sumValues()避免了大量的JNI开销。
错误认定本机代码与Java代码之间的界限
本机代码和Java代码之间的界限是由开发人员定义的。
界限的选定会对应用程序的总体性能造成显著的影响。
从Java代码中调用本机代码以及从本机代码调用Java代码的开销比普通的Java方法调用高很多。
此外,这种越界操作会干扰JVM优化代码执行的能力。
举例来说,随着Java代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。
经过测量,我们发现从Java代码调用本机代码要比普通调用多花5倍的时间。
同样,从本机代码中调用Java代码也需要耗费大量的时间。
性能技巧#5
定义Java代码与本机代码之间的界限,最大限度地减少两者之间的互相调用。
因此,在设计Java代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。
消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。
最大限度地减少越界调用的一个关键因素是确保数据处于Java/本机界限的正确一侧。
如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。
举例来说,如果我们希望使用JNI为某个串行端口提供接口,则可以构造两种不同的接口。
第一个版本如清单6所示:
清单6.到串行端口的接口:
版本1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/**
*InitializestheserialportandreturnsajavaSerialPortConfigobjects
*thatcontainsthehardwareaddressfortheserialport,andholds
*informationneededbytheserialportsuchasthenextbuffer
*towritedatainto
*
*@paramenvJNIenvthatcanbeusedbythemethod
*@paramcomPortNamethenameoftheserialport
*@returnsSerialPortConfigobjecttobepassedotsetSerialPortBit
*andgetSerialPortBitcalls
*/
jobjectinitializeSerialPort(JNIEnv*env,jobjectobj,jstringcomPortName);
/**
*Setsasinglebitinan8bitbytetobesentbytheserialport
*
*@paramenvJNIenvthatcanbeusedbythemethod
*@paramserialPortConfigobjectreturnedbyinitializeSerialPort
*@paramwhichBitvaluefrom1-8indicatingwhichbittoset
*@parambitValue0thbitcontainsbitvaluetobeset
*/
voidsetSerialPortBit(JNIEnv*env,jobjectobj,jobjectserialPortConfig,
jintwhichBit,jintbitValue);
/**
*Getsasinglebitinan8bitbytereadfromtheserialport
*
*@paramenvJNIenvthatcanbeusedbythemethod
*@paramserialPortConfigobjectreturnedbyinitializeSerialPort
*@paramwhichBitvaluefrom1-8indicatingwhichbittoread
*@returnsthebitreadinthe0thbitofthejint
*/
jintgetSerialPortBit(JNIEnv*env,jobjectobj,jobjectserialPortConfig,
jintwhichBit);
/**
*Readthenextbytefromtheserialport
*
*@paramenvJNIenvthatcanbeusedbythemethod
*/
voidreadNextByte(JNIEnv*env,jobjectobj);
/**
*Sendthenextbyte
*
*@paramenvJNIenvthatcanbeusedbythemethod
*/
voidsendNextByte(JNIEnv*env,jobjectobj);
在清单6中,串行端口的所有配置数据都存储在由initializeSerialPort()方法返回的Java对象中,并且将Java代码完全控制对硬件中各数据位的设置。
清单6所示版本的一些问题会造成其性能差于清单7中的版本:
清单7.到串行端口的接口:
版本2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/**
*Initializestheserialportandreturnsanopaquehandletoanative
*structurethatcontainsthehardwareaddressfortheserialport
*andholdsinformationneededbytheserialpor
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 使用 Java Native Interface 的最佳实践 最佳 实践